Compare commits

...

380 Commits

Author SHA1 Message Date
iequidoo
88fae8b813 feat: Don't copy References and In-Reply-To to outer headers
This implements the suggestion from
https://www.rfc-editor.org/rfc/rfc9788.html#name-offering-more-ambitious-hea of "Header Protection
for Cryptographically Protected Email".
2025-11-14 05:41:24 -03:00
iequidoo
8c09ca3691 feat: Do not copy Auto-Submitted header into outer part
Before, copying Auto-Submitted to the outer headers was needed for moving such messages,
e.g. multi-device sync messages, to the DeltaChat folder. Now all encrypted messages are moved.
2025-11-14 05:07:25 -03:00
iequidoo
a9949f87c2 feat: Do not copy Chat-Version header into outer part
Chat-Version is used sometimes by Sieve filters to move messages to DeltaChat folder:
37beed6ad9/data/conf/dovecot/global_sieve_before
This probably prevents notifications to MUAs that don't watch DeltaChat but watch INBOX.

There are however disadvantages to exposing Chat-Version:
1. Spam filters may not like this header and it is difficult or impossible to tell if `Chat-Version`
   plays role in rejecting the message or delivering it into Spam folder. If there is no such header
   visible to the spam filter, this possibility can be ruled out.
2. Replies to chat messages may have no `Chat-Version` but have to be moved anyway.
3. The user may have no control over the Sieve filter, but it comes preconfigured in mailcow, so it
   is not possible to disable it on the client.

Thanks to link2xt for providing this motivation.

NOTE: Old Delta Chat will assign partially downloaded replies to an ad-hoc group with the sender
instead of the 1:1 chat, but we're removing partial downloads anyway.
2025-11-14 05:07:25 -03:00
iequidoo
e95e40c94f feat: Move all encrypted messages to mvbox if MvboxMove is on
Before, only replies to chat messages were moved to the mvbox because we're removing Chat-Version
from outer headers, but there's no much sense in moving only replies and not moving original
messages and MDNs. Instead, move all encrypted messages. Users should be informed about this in UIs,
so if a user has another PGP-capable MUA, probably they should disable MvboxMove. Moreover, untying
this logic from References and In-Reply-To allows to remove them from outer headers too, the "Header
Protection for Cryptographically Protected Email" RFC even suggests such a behavior:
https://datatracker.ietf.org/doc/html/rfc9788#name-offering-more-ambitious-hea.
2025-11-14 05:07:25 -03:00
iequidoo
9e0e0dcaf7 feat: Don't download group messages unconditionally
There was a comment that group messages should always be downloaded to avoid inconsistent group
state, but this is solved by the group consistency algo nowadays in the sense that inconsistent
group state won't spread to other members if we send to the group. Moreover, encrypted messages are
now always downloaded, and unencrypted chat replies too, and as for ad-hoc groups,
`Config::ShowEmails` controls everything.
2025-11-14 05:07:25 -03:00
iequidoo
37d9b704dc feat: imap: Don't prefetch Chat-Version; try to find out message encryption state instead
Instead, prefetch Secure-Join, Content-Type and Subject headers, try to find out if the message is encrypted, i.e.:
- if its Content-Type is "multipart/encrypted"
- or Subject is "..." or "[...]" as some MUAs use "multipart/mixed"; we can't only look at Subject
  as it's not mandatory;
and depending on this decide on the target folder and whether the message should be
downloaded. There's no much sense in downloading unencrypted "Chat-Version"-containing messages if
`ShowEmails` is `Off` or `AcceptedContacts`, unencrypted Delta Chat messages should be considered as
usual emails, there's even the "New E-Mail" feature in UIs nowadays which sends such messages.

Don't prefetch Auto-Submitted as well, this becomes unnecessary.

Changed behavior: before, "Chat-Version"-containing messages were moved from INBOX to DeltaChat, now
such encrypted messages may remain in INBOX -- if there's no parent message or it's not
`MessengerMessage`. Don't unconditionally move encrypted messages yet because the account may be
shared with other software which doesn't and shouldn't look into the DeltaChat folder.
2025-11-14 05:07:25 -03:00
Simon Laux
dc4ea1865a fix: set get_max_smtp_rcpt_to for chatmail to the actual limit of 1000 instead of unlimited. (#7432)
adb brought this up in an internal discussion.
With the recent introduction of channels it becomes easier to hit the
limit
and it becomes impossible to send messages to a channel with more than
1000 members, this pr fixes that.

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-11-13 18:36:35 +00:00
link2xt
4b1dff601d refactor: use wait_for_incoming_msg() in more tests 2025-11-13 00:25:16 +00:00
link2xt
a66808e25a api(rpc-client): add Account.wait_for_msg() 2025-11-13 00:25:16 +00:00
link2xt
7b54954401 test: port folder-related CFFI tests to JSON-RPC
Created new test_folders.py

Moved existing JSON-RPC tests:
- test_reactions_for_a_reordering_move
- test_delete_deltachat_folder

Ported tests:
- test_move_works_on_self_sent
- test_moved_markseen
- test_markseen_message_and_mdn
- test_mvbox_thread_and_trash (renamed to test_mvbox_and_trash)
- test_scan_folders
- test_move_works
- test_move_avoids_loop
- test_immediate_autodelete
- test_trash_multiple_messages

The change also contains fixes for direct_imap fixture
needed to use IMAP IDLE in JSON-RPC tests.
2025-11-12 08:07:40 +00:00
link2xt
d39ed9d0f1 test: fix flaky test_send_receive_locations 2025-11-12 05:50:00 +00:00
iequidoo
c499dabbe1 feat: Add Contact::get_or_gen_color. Use it in CFFI and JSON-RPC to avoid gray self-color (#7374)
`Contact::get_color()` returns gray if own keypair doesn't exist yet and we don't want any UIs
displaying it. Keypair generation can't be done in `get_color()` or `get_by_id_optional()` to avoid
breaking Core tests on key import. Also this makes the API clearer, pure getters shouldn't modify
any visible state.
2025-11-12 00:30:42 -03:00
Hocuri
e70307af1f feat: Tweak initial info-message for unencrypted chats (#7427)
Fix https://github.com/chatmail/core/issues/7404
2025-11-11 19:28:28 +01:00
link2xt
69a3a31554 chore(release): prepare for 2.26.0 2025-11-11 17:30:19 +00:00
link2xt
1cb0a25e16 fix: do not ignore I/O errors in BlobObject::store_from_base64 2025-11-08 20:06:45 +00:00
iequidoo
fdea6c8af3 feat: Error toast for "Not creating securejoin QR for old broadcast" 2025-11-08 16:23:15 -03:00
link2xt
2e9fd1c25d test: do not add QR inviter to groups right after scanning the code
The inviter may be not part of the group
by the time we scan the QR code.
2025-11-08 03:26:23 +00:00
link2xt
1b1a5f170e test: Bob has 0 members in the chat until securejoin finishes 2025-11-08 03:26:23 +00:00
link2xt
1946603be6 test: at the end of securejoin Bob has two members in a group chat 2025-11-08 03:26:23 +00:00
link2xt
c43b622c23 test: move test_two_group_securejoins from receive_imf to securejoin module 2025-11-08 03:26:23 +00:00
link2xt
73bf6983b9 fix: do not add QR inviter to groups immediately
By the time you scan the QR code,
inviter may not be in the group already.
In this case securejoin protocol will never complete.
If you then join the group in some other way,
this results in you implicitly adding that inviter
to the group.
2025-11-08 03:26:23 +00:00
link2xt
aaa0f8e245 fix: do not return an error from receive_imf if we fail to add a member because we are not in chat
This happens when we receive a vg-request-with-auth message
for a chat from which we have been removed already.
2025-11-08 03:26:23 +00:00
link2xt
5a1e0e8824 chore: rustfmt 2025-11-08 03:26:23 +00:00
link2xt
cf5b145ce0 refactor: remove unused imports 2025-11-07 17:31:34 +00:00
link2xt
dd11a0e29a refactor: replace imap:: calls in migration 73 with SQL queries 2025-11-07 07:12:08 +00:00
link2xt
3d86cb5953 test: remove ThreadPoolExecutor from test_wait_next_messages 2025-11-07 07:09:35 +00:00
link2xt
75eb94e44f docs: fix Context::set_stock_translation reference 2025-11-07 06:56:10 +00:00
link2xt
7fef812b1e refactor(imap): move resync request from Context to Imap
For multiple transports we will need to run
multiple IMAP clients in parallel.
UID validity change detected by one IMAP client
should not result in UID resync
for another IMAP client.
2025-11-06 19:16:30 +00:00
link2xt
5f174ceaf2 test: test editing saved messages 2025-11-06 18:38:11 +00:00
link2xt
06b038ab5d fix: is_encrypted() should be true for Saved Messages chat
Otherwise UIs don't allow to edit messages sent to self.
This was likely broken in b417ba86bc
2025-11-06 18:38:11 +00:00
Simon Laux
b20da3cb0e docs: readme: update language binding section to avoid usage of cffi in new projects (#7380)
Updated language bindings section to reflect deprecation of
`libdeltachat and removed outdated entries.
2025-11-06 13:04:56 +00:00
Simon Laux
a3328ea2de api!(jsonrpc): chat_type now contains a variant of a string enum/union. Affected places: FullChat.chat_type, BasicChat.chat_type, ChatListItemFetchResult::ChatListItem.chat_type, Event:: SecurejoinInviterProgress.chat_type and MessageSearchResult.chat_type (#7285)
Actually it will be not as breaking if you used the constants, because
this pr also changes the constants.

closes #7029 

Note that I had to change the constants from enum to namespace, this has
the side effect, that you can no longer also use the constants as types,
you need to instead prefix them with `typeof ` now.
2025-11-06 12:53:48 +00:00
Hocuri
ee75094bef chore(release): prepare for 2.25.0 2025-11-05 17:27:00 +01:00
Hocuri
a40fd288fc fix: add info message if user tries to create a QR code for deprecated channel (#7399)
Fix https://github.com/chatmail/core/issues/7397:
- Don't allow creating a QR code for such old channels.
2025-11-05 17:16:54 +01:00
link2xt
81ba2d20d6 fix: add device message instead of partial message when receive_imf fails 2025-11-05 14:11:27 +00:00
Hocuri
f04c881b8c feat: Put self-name into group invite codes (#7398)
Fix https://github.com/chatmail/core/issues/7015 by putting the
self-name into invite codes for group and broadcast channels.

The self-name will be truncated to 16 characters.
2025-11-04 23:17:54 +01:00
bjoern
ee6b9075aa slightly nicer and shorter QR and invite codes (#7390)
- sort garbage to the beginning, readable text to the end
- instead of `%20`, make use of `+` to encode spaces
- shorter invite links and smaller QR codes by truncation of the names

the truncation of the name uses chars() which does not respect grapheme clusters, so
that last character may be wrong. not sure if there is a nice and easy
alternative, but maybe it's good engoug - the real, full name will come
over the wire (exiting truncate() truncates on word boundaries, which is
maybe too soft here - names may be long, depending on the language, and
not contain any space)

moreover, this resolves the "name too long" issue from
https://github.com/chatmail/core/issues/7015

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-11-04 22:01:24 +01:00
link2xt
9c2a13b88e refactor(sql): do not expose rusqlite Error type in query_map methods
We use query_and_then() instead of query_map() function now.
The difference is that row processing function
returns anyhow::Result, so simple fallible processing
like JSON parsing can be done inside of it
when calling query_map_vec() and query_map_collect()
without having to resort to query_map()
and iterating over all rows again afterwards.
2025-11-03 23:08:56 +00:00
dependabot[bot]
1db6ea70cc chore(deps): bump astral-sh/setup-uv from 7.1.0 to 7.1.2
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.0 to 7.1.2.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](3259c6206f...85856786d1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 22:08:18 +00:00
dependabot[bot]
da2d9620cd chore(deps): bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 22:08:00 +00:00
dependabot[bot]
d1dcb739f2 chore(deps): bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 22:07:43 +00:00
Hocuri
e34687ba42 core(release): prepare for v2.24.0 2025-11-03 22:12:36 +01:00
Hocuri
5034449009 feat!: QR codes and symmetric encryption for broadcast channels (#7268)
Follow-up for https://github.com/chatmail/core/pull/7042, part of
https://github.com/chatmail/core/issues/6884.

This will make it possible to create invite-QR codes for broadcast
channels, and make them symmetrically end-to-end encrypted.

- [x] Go through all the changes in #7042, and check which ones I still
need, and revert all other changes
- [x] Use the classical Securejoin protocol, rather than the new 2-step
protocol
- [x] Make the Rust tests pass
- [x] Make the Python tests pass
- [x] Fix TODOs in the code
- [x] Test it, and fix any bugs I find
- [x] I found a bug when exporting all profiles at once fails sometimes,
though this bug is unrelated to channels:
https://github.com/chatmail/core/issues/7281
- [x] Do a self-review (i.e. read all changes, and check if I see some
things that should be changed)
- [x] Have this PR reviewed and merged
- [ ] Open an issue for "TODO: There is a known bug in the securejoin
protocol"
- [ ] Create an issue that outlines how we can improve the Securejoin
protocol in the future (I don't have the time to do this right now, but
want to do it sometime in winter)
- [ ] Write a guide for UIs how to adapt to the changes (see
https://github.com/deltachat/deltachat-android/pull/3886)

## Backwards compatibility

This is not very backwards compatible:
- Trying to join a symmetrically-encrypted broadcast channel with an old
device will fail
- If you joined a symmetrically-encrypted broadcast channel with one
device, and use an old core on the other device, then the other device
will show a mostly empty chat (except for two device messages)
- If you created a broadcast channel in the past, then you will get an
error message when trying to send into the channel:

> The up to now "experimental channels feature" is about to become an officially supported one. By that, privacy will be improved, it will become faster, and less traffic will be consumed.
> 
> As we do not guarantee feature-stability for such experiments, this means, that you will need to create the channel again. 
> 
> Here is what to do:
>  • Create a new channel
>  • Tap on the channel name
>  • Tap on "QR Invite Code"
>  • Have all recipients scan the QR code, or send them the link
> 
> If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/.


## The symmetric encryption

Symmetric encryption uses a shared secret. Currently, we use AES128 for
encryption everywhere in Delta Chat, so, this is what I'm using for
broadcast channels (though it wouldn't be hard to switch to AES256).

The secret shared between all members of a broadcast channel has 258
bits of entropy (see `fn create_broadcast_shared_secret` in the code).

Since the shared secrets have more entropy than the AES session keys,
it's not necessary to have a hard-to-compute string2key algorithm, so,
I'm using the string2key algorithm `salted`. This is fast enough that
Delta Chat can just try out all known shared secrets. [^1] In order to
prevent DOS attacks, Delta Chat will not attempt to decrypt with a
string2key algorithm other than `salted` [^2].

## The "Securejoin" protocol that adds members to the channel after they
scanned a QR code

This PR uses the classical securejoin protocol, the same that is also
used for group and 1:1 invitations.

The messages sent back and forth are called `vg-request`,
`vg-auth-required`, `vg-request-with-auth`, and `vg-member-added`. I
considered using the `vc-` prefix, because from a protocol-POV, the
distinction between `vc-` and `vg-` isn't important (as @link2xt pointed
out in an in-person discussion), but
1. it would be weird if groups used `vg-` while broadcasts and 1:1 chats
used `vc-`,
2. we don't have a `vc-member-added` message yet, so, this would mean
one more different kind of message
3. we anyways want to switch to a new securejoin protocol soon, which
will be a backwards incompatible change with a transition phase. When we
do this change, we can make everything `vc-`.



[^1]: In a symmetrically encrypted message, it's not visible which
secret was used to encrypt without trying out all secrets. If this does
turn out to be too slow in the future, then we can remember which secret
was used more recently, and and try the most recent secret first. If
this is still too slow, then we can assign a short, non-unique (~2
characters) id to every shared secret, and send it in cleartext. The
receiving Delta Chat will then only try out shared secrets with this id.
Of course, this would leak a little bit of metadata in cleartext, so, I
would like to avoid it.
[^2]: A DOS attacker could send a message with a lot of encrypted
session keys, all of which use a very hard-to-compute string2key
algorithm. Delta Chat would then try to decrypt all of the encrypted
session keys with all of the known shared secrets. In order to prevent
this, as I said, Delta Chat will not attempt to decrypt with a
string2key algorithm other than `salted`

BREAKING CHANGE: A new QR type AskJoinBroadcast; cloning a broadcast
channel is no longer possible; manually adding a member to a broadcast
channel is no longer possible (only by having them scan a QR code)
2025-11-03 21:02:13 +01:00
link2xt
997e8216bf refactor: split "transport" module out of "login_param"
`login_param` module is now for user-visible entered login parameters,
while the `transport` module contains structures for internal
representation of connection candidate list
created during transport configuration.
2025-11-03 18:58:36 +00:00
iequidoo
7f059140be docs: Comment why spaced en dash is used to separate message Subject from text 2025-11-01 14:06:55 -03:00
link2xt
c9b3da4a1a chore(release): prepare for 2.23.0 2025-11-01 16:03:01 +00:00
link2xt
098084b9a7 feat: temporarily disable OpenPGP recipient anonymization 2025-11-01 15:27:00 +00:00
Simon Laux
9bc2aeebb8 feat: show if proxy is enabled in connectivity view (#7359)
closes #7269
2025-10-31 23:53:05 +01:00
link2xt
56370c2f90 ci: update Rust to 1.91.0 2025-10-31 12:33:47 +00:00
link2xt
59959259bf chore: fix Rust 1.91.0 lint for derivable Default 2025-10-31 12:33:47 +00:00
link2xt
08f8f488b1 refactor: remove unused call to get_credentials() 2025-10-31 12:33:35 +00:00
link2xt
f34311d5c4 build: do not install pdbpp in the test environment for CFFI Python bindings
Closes <https://github.com/chatmail/core/issues/7376>
2025-10-31 12:33:23 +00:00
link2xt
885a5efa39 fix: stop notifying about messages in contact request chats 2025-10-31 07:31:35 +00:00
Hocuri
8b4c718b6b feat(backwards-compat): For now, send Chat-Verified header (instead of _verified) again 2025-10-29 14:52:54 +00:00
link2xt
2ada3cd613 fix: stop using leftgrps table 2025-10-28 19:41:47 +00:00
Simon Laux
b920552fc3 api: jsonrpc: typescript remove unused constants (#7355) 2025-10-28 17:59:32 +01:00
Simon Laux
92c31903c6 api: jsonrpc: add get_push_state to check push notification state (#7356) 2025-10-28 17:58:11 +01:00
dependabot[bot]
145145f0fb chore(deps): bump cachix/install-nix-action from 31.8.0 to 31.8.1
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.8.0 to 31.8.1.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](7ab6e7fd29...fd24c48048)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-28 12:28:33 +00:00
link2xt
05ba206c5a feat: allow plain domain in dcaccount: scheme
This is similar to old `dcaccount:` with URL,
but creates a 9-character username on the client
and avoids making an HTTPS request.

The scheme is reused to avoid the apps
needing to register for the new scheme.

`http` support is removed because it was
not working already, there is a check
that the scheme is `https` when the URL
is actually used and the core has
no way to make HTTP requests without TLS.
2025-10-28 12:10:52 +00:00
link2xt
9f0d106818 api(deltachat-rpc-client): add Account.add_transport_from_qr() API 2025-10-28 12:10:52 +00:00
link2xt
21caf87119 refactor: use SampleString 2025-10-28 12:10:52 +00:00
link2xt
4abc695790 refactor: use rand::fill() instead of rand::rng().fill() 2025-10-28 12:10:52 +00:00
dependabot[bot]
df1a7ca386 chore(deps): bump actions/setup-node from 5 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 22:20:58 +00:00
iequidoo
a06ba35ce1 feat: Remove Config::ConfiguredSentboxFolder and everything related
It's used in `fetch_existing_msgs()`, but we can remove it and tell users that they need to
move/copy messages from Sentbox to Inbox so that Delta Chat adds all contacts from them. This way
users will be also informed that Delta Chat needs users to CC/BCC/To themselves to see messages sent
from other MUAs.
2025-10-26 14:17:07 -03:00
iequidoo
18445c09c2 feat: Remove Config::SentboxWatch (#7178)
The motivation is to reduce code complexity, get rid of the extra IMAP connection and cases when
messages are added to chats by Inbox and Sentbox loops in parallel which leads to various message
sorting bugs, particularly to outgoing messages breaking sorting of incoming ones which are fetched
later, but may have a smaller "Date".
2025-10-26 14:17:07 -03:00
link2xt
f428033d95 build: update rand to 0.9
We already have both rand 0.8 and rand 0.9
in our dependency tree.

We still need rand 0.8 because
public APIs of rPGP 0.17.0 and Iroh 0.35.0
use rand 0.8 types in public APIs,
so it is imported as rand_old.
2025-10-26 07:08:54 +00:00
link2xt
0e30dd895f fix: fix flaky tests::verified_chats::test_verified_chat_editor_reordering and receive_imf::receive_imf_tests::test_two_group_securejoins
Flakiness was introduced in e7348a4fd8.
This change removes a call to joining_chat_id() which created a chat,
now we check for existing group chat
without creating it if it does not exist.
2025-10-25 06:47:17 +00:00
link2xt
c001a9a983 refactor(sql): add query_map_collect() 2025-10-24 18:17:15 +00:00
link2xt
5f3948b462 refactor(sql): add query_map_vec()
This also replaces some cases where flatten()
was used, effectively ignoring the errors.
2025-10-24 18:17:15 +00:00
link2xt
45a1d81805 refactor(sql): change second query_map function from FnMut to FnOnce
FnOnce is a supertrait of FnMut,
so any argument that implements FnMut
can also be used in a place where FnOnce is required.
2025-10-24 18:17:15 +00:00
Hocuri
19d7799324 feat: Be more generous with marking contacts as verified for now (#7336)
Context: PR #7116 is backwards-incompatible with versions older than
v2.21, and since the release hasn't reached all users yet, we currently
can't release from main; for details see #7326.

Issue #7326 explains how we can make this less breaking, but this only
works if many contacts are verified. So, this PR here proposes to
postpone the stricter rules for who is verified a bit:

- Set verification timeout for invite codes to 1 week (this is still
stricter than no timeout at all, which we had in the past)
- Don't reset indirect verifications yet

In a few months (when everyone has v2.22.0), we can revert the PR here,
then.

---------

Co-authored-by: l <link2xt@testrun.org>
2025-10-24 18:07:29 +00:00
iequidoo
24e18c1485 fix: Delete obsolete "configured*" keys from config table (#7171) 2025-10-24 02:15:25 -03:00
link2xt
3eb1a7dfac feat: protect the Date header 2025-10-23 15:29:14 +00:00
link2xt
2f2a147efb feat: move the messages only from INBOX and Spam folders
We do not try to delete resent messages
anymore. Previously resent messages
were distinguised by having duplicate Message-ID,
but future Date, but now we need to download
the message before we even see the Date.
We now move the message to the destination folder
but do not fetch it.

It may not be a good idea to delete
the duplicate in multi-device setups anyway,
because the device which has a message
may delete the duplicate of a message
the other device missed.

To avoid triggering IMAP busy move loop
described in the comments
we now only move the messages
from INBOX and Spam folders.
2025-10-23 15:29:14 +00:00
link2xt
f4938465c3 feat(deltachat-rpc-client): support multiple transports in resetup_account() 2025-10-23 15:14:53 +00:00
WofWca
129137b5de fix(jsonrpc): fix ChatListItem::is_self_in_group
Make it return the correct value for non-Group chats.

This also should improve performance, thanks to the fact that
we now don't have to query all the chat's contacts.
Instead we only need confirm that self-contact
is among the group members, and only when the chat type is `Group`.
2025-10-23 16:52:32 +02:00
link2xt
ec3f765727 refactor(add_transport_from_qr): do not set deprecated config values 2025-10-23 14:04:53 +00:00
link2xt
a743ad9490 feat: do not send Autocrypt in MDNs 2025-10-23 13:49:40 +00:00
link2xt
89315b8ef2 refactor: stop using deprecated Account.configure() 2025-10-23 13:41:57 +00:00
link2xt
e7348a4fd8 feat: do not run SecureJoin if we are already in the group 2025-10-23 06:53:22 +00:00
link2xt
c68244692d api(jsonrpc): restore protect argument for create_group_chat
It was removed in 498a831873
Restoring as optional argument to avoid breaking compatibility.
2025-10-22 18:14:21 +00:00
link2xt
3c93f61b4d fix: migrate from tokio-tar to astral-tokio-tar
tokio-tar is unmaintained and has unpatched CVE-2025-62518.
More details on CVE are in <https://edera.dev/stories/tarmageddon>.
tokio-tar is only used for transferring backups
and worst case is that by manually inspecting
a carefully crafted backup user will not see
the same files as get unpacked when importing a backup.
2025-10-22 16:09:21 +00:00
Hocuri
51b9e86d71 Opt-in weekly sending of statistics (#6851)
This way, the statistics / self-reporting bot will be made into an
opt-in regular sending of statistics, where you enable the setting once
and then they will be sent automatically. The statistics will be sent to
a bot, so that the user can see exactly which data is being sent, and
how often. The chat will be archived and muted by default, so that it
doesn't disturb the user.

The collected statistics will focus on the public-key-verification that
is performed while scanning a QR code. Later on, we can add more
statistics to collect.

**Context:**

_This is just to give a rough idea; I would need to write a lot more
than a few paragraphs in order to fully explain all the context here_.

End-to-end encrypted messengers are generally susceptible to MitM
attacks. In order to mitigate against this, messengers offer some way of
verifying the chat partner's public key. However, numerous studies found
that most popular messengers implement this public-key-verification in a
way that is not understood by users, and therefore ineffective - [a 2021
"State of Knowledge" paper
concludes:](https://dl.acm.org/doi/pdf/10.1145/3558482.3581773)

> Based on our evaluation, we have determined that all current E2EE
apps, particularly when operating in opportunistic E2EE mode, are
incapable of repelling active man-in-the-middle (MitM) attacks. In
addition, we find that none of the current E2EE apps provide better and
more usable [public key verification] ceremonies, resulting in insecure
E2EE communications against active MitM attacks.

This is why Delta Chat tries to go a different route: When the user
scans a QR code (regardless of whether the QR code creates a 1:1 chat,
invites to a group, or subscribes to a broadcast channel), a
public-key-verification is performed in the background, without the user
even having to know about this.

The statistics collected here are supposed to tell us whether Delta Chat
succeeds to nudge the users into using QR codes in a way that is secure
against MitM attacks.

**Plan for statistics-sending:**

- [x] Get this PR reviewed and merged (but don't make it available in
the UI yet; if Android wants to make a release in the meantime, I will
create a PR that removes the option there)
- [x] Set the interval to 1 week again (right now, it's 1 minute for
testing)
- [ ] Write something for people who are interested in what exactly we
count, and link to it (see `TODO[blog post]` in the code)
- [ ] Prepare a short survey for participants
- [ ] Fine-tune the texts at
https://github.com/deltachat/deltachat-android/pull/3794, and get it
reviewed and merged
- [ ] After the next release, ask people to enable the
statistics-sending
2025-10-21 15:29:21 +02:00
Simon Laux
347938a9f9 refactor: jsonrpc rename change casing in names of jsonrpc structs/enums to comply with rust naming conventions. (#7324) 2025-10-21 13:49:16 +02:00
Hocuri
9897ef2e9b refactor: Remove error stock strings that are rarely used these days (#7327)
This removes the DC_STR_CANTDECRYPT_MSG_BODY and
DC_STR_CANT_DECRYPT_OUTGOING_MSGS stock strings.

See
https://github.com/deltachat/deltachat-android/pull/3956#issuecomment-3425349195
for reasoning.
2025-10-21 09:04:11 +00:00
link2xt
2f34a740c7 build(nix): fix build of deltachat-rpc-server-x86_64-darwin 2025-10-20 20:29:47 +00:00
iequidoo
fc81cef113 refactor: Rename chat::create_group_chat() to create_group()
If we use modules (which are actually namespaces), we can use shorter names. Another approach is to
only use modules for internal code incapsulation and use full names like deltachat-ffi does.
2025-10-20 04:19:22 -03:00
iequidoo
04c2585c27 feat: Synchronize encrypted groups creation across devices (#7001)
Unencrypted groups don't have grpid since key-contacts were merged, so we don't sync them for now.
2025-10-20 04:19:22 -03:00
Simon Laux
59fac54f7b test: Add unique offsets to ids generated by TestContext to increase test correctness (#7297)
and fix the mistakes in tests that get discovered by this.

closes #6799
2025-10-19 17:08:23 +00:00
Simon Laux
65b61efb31 build: ignore configuration for the zed editor (#7322) 2025-10-19 17:03:39 +00:00
link2xt
afc74b0829 fix: do not allow sync item timestamps to be in the future 2025-10-19 11:35:10 +00:00
link2xt
2481a0f48e feat: reset all indirect verifications 2025-10-19 11:35:09 +00:00
link2xt
6c24edb40d feat: do not mark Bob as verified if auth token is old 2025-10-19 11:35:09 +00:00
link2xt
e4178789da refactor: remove ProtectionStatus 2025-10-19 11:35:09 +00:00
link2xt
b417ba86bc api!: remove Chat.is_protected() 2025-10-19 11:35:09 +00:00
link2xt
498a831873 api!: remove APIs to create protected chats
Create unprotected group in test_create_protected_grp_multidev
The test is renamed accordingly.

SystemMessage::ChatE2ee is added in encrypted groups
regardless of whether they are protected or not.
Previously new encrypted unprotected groups
had no message saying that messages are end-to-end encrypted
at all.
2025-10-19 11:35:09 +00:00
link2xt
c6722d36de api!: remove public APIs to check if the chat is protected 2025-10-19 11:35:09 +00:00
link2xt
90f0d5c060 api: make dc_chat_is_protected always return 0 2025-10-19 11:35:09 +00:00
link2xt
90ec2f2518 docs: document Autocrypt-Gossip _verified attribute 2025-10-19 11:35:09 +00:00
link2xt
5b66535134 feat: verify contacts via Autocrypt-Gossip
This mechanism replaces `Chat-Verified` header.
New parameter `_verified=1` in `Autocrypt-Gossip`
header marks that the sender has the gossiped key
verified.

Using `_verified=1` instead of `_verified`
because it is less likely to cause troubles
with existing Autocrypt header parsers.
This is also how https://www.rfc-editor.org/rfc/rfc2045
defines parameter syntax.
2025-10-19 11:35:09 +00:00
link2xt
eea848f72b fix: don't ignore QR token timestamp from sync messages 2025-10-19 11:35:09 +00:00
link2xt
214a1d3e2d feat: do not resolve MX records during configuration
MX record lookup was only used to detect Google Workspace domains.
They can still be configured manually.
We anyway do not want to encourage creating new profiles
with Google Workspace as we don't have Gmail OAUTH2 token anymore
and new users can more easily onboard with a chatmail relay.
2025-10-18 18:09:56 +00:00
link2xt
e270a502d1 refactor: remove invalid Gmail OAuth2 tokens
They were already unused since
<https://github.com/chatmail/provider-db/pull/310>
2025-10-18 18:09:56 +00:00
iequidoo
b863345600 test(rpc-client): vCard color is the same as the contact color (#7294)
This tests "fix(jsonrpc): Use Core's logic for computing VcardContact.color".
2025-10-17 14:46:57 -03:00
link2xt
61b49a9339 Merge tag 'v2.22.0'
Release 2.22.0
2025-10-17 10:53:12 +00:00
link2xt
41c80cf3f2 chore(release): prepare for 2.22.0 2025-10-17 10:51:26 +00:00
l
6fd3645360 Merge pull request #7320 from chatmail/link2xt/dont-notify-contact-request-calls 2025-10-17 10:45:41 +00:00
link2xt
b812d0a7f7 fix: do not notify about incoming calls for contact requests and blocked contacts 2025-10-17 10:30:46 +00:00
link2xt
e8a4c9237d test: accept the chat with the caller before accepting calls 2025-10-17 10:30:46 +00:00
link2xt
5256013615 feat: protect Autocrypt header 2025-10-16 23:34:44 +00:00
link2xt
9826c28581 feat: anonymize OpenPGP recipients 2025-10-16 23:34:03 +00:00
link2xt
9ceceebdc3 ci: set 7 days cooldown on Dependabot updates
This fixes the warning
<https://docs.zizmor.sh/audits/#dependabot-cooldown>
and avoids updating to freshly published dependencies
that are more likely to be unpublished.
2025-10-16 20:50:24 +00:00
link2xt
187d913f84 ci: pin GitHub action astral-sh/setup-uv
Pinned version corresponds to the current v7.1.0 tag.
2025-10-16 20:50:03 +00:00
link2xt
4a0b180d86 chore(release): prepare for 2.21.0 2025-10-16 18:18:10 +00:00
dependabot[bot]
6fa6055912 chore(deps): bump github/codeql-action from 3 to 4 (#7304) 2025-10-16 02:24:19 +00:00
dependabot[bot]
667995cde4 chore(cargo): bump async_zip from 0.0.17 to 0.0.18 (#7257) 2025-10-15 21:54:02 +00:00
link2xt
1e0def87fd feat: cache tile.openstreetmap.org tiles for 7 days 2025-10-15 17:07:55 +00:00
link2xt
a219e5ee8c feat: set User-Agent for tile.openstreetmap.org requests 2025-10-15 17:07:55 +00:00
link2xt
8070dfcc82 refactor(mimeparser): store only one signature fingerprint
Messages are normally not signed with more than one key
and in this case we pick an arbitrary signature later anyway.
2025-10-15 16:45:36 +00:00
link2xt
176a89bd03 feat(deltachat-repl): add send-sync command
This allows to send send messages when disconnected
using `send_msg_sync` to create a dedicated connection
and send a single message.

The command can be used for measuring
the duration of SMTP connection establishment.
2025-10-15 16:45:19 +00:00
link2xt
dd8dd2f95c build(nix): remove unused dependencies
darwin.apple_sdk.frameworks is deprecated and does nothing according to
<https://nixos.org/manual/nixpkgs/stable/#sec-darwin-legacy-frameworks>

libiconv dependency also does not seem to be needed for `libdeltachat`,
I have checked that `nix build .#libdeltachat` still succeeds on macOS.
2025-10-15 16:43:54 +00:00
link2xt
eb1bd1d200 feat: TLS 1.3 session resumption 2025-10-15 16:41:50 +00:00
link2xt
460d2f3c2a refactor: pass ALPN around as &str 2025-10-15 16:41:50 +00:00
link2xt
0ab10f99fd refactor: use rustls reexported from tokio_rustls 2025-10-15 16:41:50 +00:00
iequidoo
377f57f1c3 fix(jsonrpc): Use Core's logic for computing VcardContact.color (#7294)
Before, the color was computed from the address, but as we've switched to fingerprint-based contact
colors, this logic became stale. Now `deltachat::contact::get_color()` is used. A test would be nice
to have, but as now all the logic is in Core, this isn't critical as there are Core tests at least.
2025-10-14 05:43:10 -03:00
iequidoo
caf5f1f619 fix: Remove Exif with non-fatal errors from images
NB: This still doesn't help with detecting Exif in PNGs unfortunately.
2025-10-14 02:13:03 -03:00
dependabot[bot]
d9ff85a202 chore(deps): bump cachix/install-nix-action from 31.7.0 to 31.8.0
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.7.0 to 31.8.0.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](9280e7aca8...7ab6e7fd29)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-14 00:59:07 +00:00
link2xt
f180a7c024 test: test expiration of non-ephemeral message with unknown viewtype 2025-10-13 18:51:46 +00:00
link2xt
7fac9332e1 test: test expiration of ephemeral messages with unknown viewtype 2025-10-13 18:51:46 +00:00
link2xt
8dd7c42f69 chore(release): prepare for 2.20.0 2025-10-13 11:34:44 +00:00
B. Petersen
b542eeecc0 fix: accept unknown viewtype in delete-old-messages loop 2025-10-13 13:29:57 +02:00
B. Petersen
bee8295daa fix: accept unknown viewtype in ephemeral loop 2025-10-13 13:29:57 +02:00
link2xt
ab9fd3d5ed chore(release): prepare for 2.19.0 2025-10-12 17:33:43 +00:00
link2xt
cc54a3feda fix: do not try to process calls from partial messages
Any control information from the message
should only be downloaded when the message
is fully downloaded to avoid processing it twice.
Besides, "partial" messages may actually be full messages
with an error that are only processed as partial
to add a message bubble allowing to download the message later.
2025-10-12 17:20:21 +00:00
link2xt
94984f35ec fix: do not fail to receive call accepted/ended messages referring to non-call Message-ID
In-Reply-To may refer to non-call message
as we do not control the sender.
It may also happen that call message
was received by older version and processed
as text, in which case correct In-Reply-To
appears to be referring to the text message.
2025-10-12 17:20:21 +00:00
iequidoo
0e47e89d63 fix: Emit AccountsItemChanged when own key is generated/imported, use gray self-color until that (#7296)
Emitting an `AccountsItemChanged` event is needed for UIs to know when `Contact::get_color()` starts
returning the true color for `SELF`. Before, an address-based color was returned for a new account
which was changing to a fingerprint-based color e.g. on app restart. Now the self-color is our
favorite gray until own keypair is generated or imported e.g. via ASM.
2025-10-12 14:17:24 -03:00
link2xt
2d7dc7a1be fix: do not fail to fully download previously trashed messages 2025-10-12 13:12:00 +00:00
iequidoo
4d76a5b599 refactor: set_chat_profile_image(): Remove !chat.is_mailing_list() check 2025-10-12 01:41:31 -03:00
link2xt
87035ff744 feat: slightly increase saturation of colors
It really depends on the screen,
but on Android phones with higher DPI
than laptop screen the contrast
looks a bit too low.

It should not be increased too high
because of clipping.
2025-10-09 20:28:21 +00:00
iequidoo
e0d123f732 chore(cargo): bump quick-xml from 0.37.5 to 0.38.3 2025-10-09 14:03:56 -03:00
link2xt
8eddcfc9d2 ci: update to Python 3.14
I have not updated Python interpreters for legacy bindings
in scripts/run_all.sh because I have not checked
if manylinux images already exist and work for building wheels.
Legacy Python bindings are deprecated
and we don't publish new releases for them.
2025-10-09 15:55:59 +00:00
Simon Laux
af58b86b60 refactor: Use variables directly in formatted strings (#7284)
made with `cargo clippy --all --fix` then manually reviewed to ensure
this was the only thing that changed.
2025-10-09 15:26:59 +00:00
Simon Laux
00ae7ce33c add nodejs to nix dev env (#7283)
it is required to build jsonrpc client npm package and for the stdio
server npm package
2025-10-09 14:27:12 +00:00
link2xt
0bc9fe841a chore(release): prepare for 2.18.0 2025-10-08 02:01:02 +00:00
iequidoo
e37920ed4e feat: No implicit member changes from old Delta Chat clients (#7220)
Old Delta Chat clients don't provide timestamps for added and removed members in messages, so at
least implicit member changes may be ignored as it's not clear if they are newer than explicit
member changes from modern clients. Lost messages aren't so frequent anyway, and overall
compatibility with old versions may be limited already.
2025-10-07 20:27:14 -03:00
iequidoo
6a7466df93 fix: Only omit group changes messages if SELF is really added (#7220)
If a self-addition message is received, but we're already in the group, there must be no hidden
member changes.
2025-10-07 20:27:14 -03:00
link2xt
1bb966e5a8 ci(nix): switch from DeterminateSystems/nix-installer-action to cachix/install-nix-action
Determinate Systems GitHub action
installs Nix version from Determinate Systems
and Determinate Nix Installer has
dropped support for installing upstream Nix:
https://determinate.systems/blog/installer-dropping-upstream/

This commit switches to upstream Nix
to avoid accidentally depending on any features
of Determinate Nix.
2025-10-07 15:56:02 +00:00
link2xt
34e631395f ci(nix): run the workflow when workflow file changes 2025-10-07 15:56:02 +00:00
link2xt
080ddde68d refactor: assert that Iroh node addresses have home relay URL
With newer Iroh it is possible to obtain
own node address before home relay is selected
and accidentally send own address without relay URL.

It took me some time to debug why Iroh 0.92.0
did not work with iroh-relay 0.92.0
so I'm adding these assertions even
while we still use Iroh 0.35.0.
2025-10-07 15:53:37 +00:00
link2xt
209a8026fb fix: do not fail to load messages with unknown viewtype 2025-10-06 15:29:24 +00:00
link2xt
23bfa4fc43 api!: remove APIs for video chat invitations 2025-10-05 12:19:10 +00:00
link2xt
58d40c118c chore(release): prepare for 2.17.0 2025-10-04 00:41:56 +00:00
link2xt
9d39769445 refactor: return the reason when failing to place calls 2025-10-04 00:23:46 +00:00
link2xt
bfc08abe88 fix: forward calls as text messages 2025-10-04 00:23:46 +00:00
link2xt
6a7b097273 fix: lowercase "call" in "Missed call" and similar strings
This string is used in summaries
and message bubbles and is typically not capitalized,
unlike menu items.
2025-10-03 15:06:29 +00:00
B. Petersen
8f2390ac99 feat: add new strings to ffi 2025-10-03 15:19:02 +02:00
iequidoo
481f5cae22 fix: Prefetch messages in limited batches (#6915)
I have logs from a user where messages are prefetched for long minutes, and while it's not a problem
on its own, we can't rely that the connection overlives such a period, so make
`fetch_new_messages()` prefetch (and then actually download) messages in batches of 500 messages.
2025-10-02 16:33:10 -03:00
iequidoo
b9068b95b8 feat(ffi): Add DC_STR_CANT_DECRYPT_OUTGOING_MSGS define 2025-10-02 16:18:57 -03:00
link2xt
df2c35b551 ci: fix CI checking Nix formatting
`nix fmt` by itself tries to read stdin,
not `flake.nix`.
2025-10-02 18:39:16 +00:00
link2xt
3cd4152a3c api!: remove deprecated verified_one_on_one_chats config 2025-10-02 18:35:12 +00:00
WofWca
2534510f0b docs: add docs for JS BaseDeltaChat
Regarding the `@deprecated` addition:
it's not clear to me why the `listAccounts` function exists,
why `getAllAccounts` gets special treatment.
It has been there ever since the introduction of JSON-RPC.
Maybe it's because of the existence of `getContextEvents`,
which has to do with accounts.
But hopefully the new doc on `getContextEvents`
compensates for this.
2025-10-02 16:48:56 +00:00
bjoern
3f8aa4635e docs: clarify CALL events (#7188)
this PR clarifies some events, also updating to recent changes.

see changed code for details

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-10-02 16:48:16 +00:00
B. Petersen
ada59e8205 docs: comment about outdated timespan 2025-10-02 16:45:06 +00:00
Hocuri
9ec0332483 feat: Add strings 'You left the channel.' and 'Scan to join Channel' (#7266)
Close https://github.com/chatmail/core/issues/7233

Part of https://github.com/chatmail/core/issues/6884
2025-10-02 14:57:24 +00:00
link2xt
d509b0cf5c ci: require that Cargo.lock is up to date
Versions in the lockfile should
be compatible with MSRV
and not automatically downgraded in CI.
2025-10-02 12:35:25 +00:00
link2xt
4d624d8c3a Revert "chore(cargo): bump sdp from 0.8.0 to 0.10.0"
This reverts commit eae1ba258a.
2025-10-02 12:35:25 +00:00
link2xt
9f0ba4b9c2 feat: better summary for calls 2025-10-02 11:19:48 +00:00
link2xt
a930ae27be api: stock strings for calls 2025-10-02 11:19:48 +00:00
link2xt
38e4919be1 api!: consistent spelling of "canceled" with a single "l"
This is how it is spelled in iOS UI and Android APIs.
2025-10-02 11:19:48 +00:00
dependabot[bot]
a668047f75 Merge pull request #7259 from chatmail/dependabot/cargo/sdp-0.10.0 2025-10-02 09:18:16 +00:00
link2xt
c2ea2cda4c feat: make text/calendar alternative available as an attachment 2025-10-02 09:15:25 +00:00
iequidoo
f3c3a2c301 Revert "chore(cargo): bump quick-xml from 0.37.5 to 0.38.3"
This reverts commit 58e1fa5c36.
2025-10-02 05:34:05 -03:00
dependabot[bot]
0da7e587a7 Merge pull request #7247 from chatmail/dependabot/cargo/image-0.25.8 2025-10-02 07:32:30 +00:00
dependabot[bot]
e6e686aaf4 Merge pull request #7255 from chatmail/dependabot/cargo/libc-0.2.176 2025-10-02 07:24:24 +00:00
dependabot[bot]
58e1fa5c36 chore(cargo): bump quick-xml from 0.37.5 to 0.38.3
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.37.5 to 0.38.3.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.37.5...v0.38.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 04:19:04 -03:00
dependabot[bot]
42549526c7 chore(cargo): bump tempfile from 3.21.0 to 3.23.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.21.0 to 3.23.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.21.0...v3.23.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 04:10:02 -03:00
dependabot[bot]
9fe1c8fe80 Merge pull request #7254 from chatmail/dependabot/cargo/toml-0.9.7 2025-10-02 07:08:01 +00:00
dependabot[bot]
b8dbcb3dbd chore(cargo): bump hyper-util from 0.1.16 to 0.1.17
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.16 to 0.1.17.
- [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.16...v0.1.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 03:59:30 -03:00
dependabot[bot]
7c5675670a chore(cargo): bump chrono from 0.4.41 to 0.4.42
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.41 to 0.4.42.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.41...v0.4.42)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 03:57:23 -03:00
dependabot[bot]
291945a4fd chore(cargo): bump log from 0.4.27 to 0.4.28
Bumps [log](https://github.com/rust-lang/log) from 0.4.27 to 0.4.28.
- [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.27...0.4.28)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 03:45:52 -03:00
dependabot[bot]
439e8827bd Merge pull request #7243 from chatmail/dependabot/cargo/quote-1.0.41 2025-10-02 06:45:16 +00:00
link2xt
a745cf78ee test: test reception of multipart/alternative with text/calendar
In messages with text/plain, text/html and text/calendar parts
within a multipart/alternative, text/calendar part is currently ignored,
text/plain is displayed and HTML is available via HTML API.
2025-10-02 02:20:17 +00:00
link2xt
af69756df0 fix: prefer last part in multipart/alternative
This is recommended by RFC 2046
and does not require a separate loop
looking for multipart subpart.
2025-10-02 02:20:17 +00:00
dependabot[bot]
46c42ab6e4 chore(cargo): bump quote from 1.0.40 to 1.0.41
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.40 to 1.0.41.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.40...1.0.41)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 01:15:56 +00:00
dependabot[bot]
33a127187b chore(cargo): bump thiserror from 2.0.16 to 2.0.17
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 2.0.16 to 2.0.17.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.16...2.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 22:13:43 -03:00
dependabot[bot]
24ddbdd251 chore(cargo): bump proptest from 1.7.0 to 1.8.0
Bumps [proptest](https://github.com/proptest-rs/proptest) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/proptest-rs/proptest/releases)
- [Changelog](https://github.com/proptest-rs/proptest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/proptest-rs/proptest/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: proptest
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:58:39 -03:00
dependabot[bot]
0122a98eea chore(cargo): bump anyhow from 1.0.99 to 1.0.100
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.99 to 1.0.100.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.99...1.0.100)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:53:53 -03:00
dependabot[bot]
406545c1f1 chore(cargo): bump uuid from 1.18.0 to 1.18.1
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.18.0...v1.18.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:53:23 -03:00
dependabot[bot]
a1b593027b Merge pull request #7241 from chatmail/dependabot/cargo/serde_json-1.0.145 2025-10-02 00:50:51 +00:00
dependabot[bot]
eae1ba258a chore(cargo): bump sdp from 0.8.0 to 0.10.0
Bumps [sdp](https://github.com/webrtc-rs/webrtc) from 0.8.0 to 0.10.0.
- [Release notes](https://github.com/webrtc-rs/webrtc/releases)
- [Commits](https://github.com/webrtc-rs/webrtc/compare/v0.8.0...v0.10.0)

---
updated-dependencies:
- dependency-name: sdp
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:07:13 +00:00
dependabot[bot]
9fb7c52217 chore(cargo): bump toml from 0.9.5 to 0.9.7
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.5 to 0.9.7.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.5...toml-v0.9.7)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:06:53 +00:00
dependabot[bot]
6cab1786d3 chore(cargo): bump image from 0.25.6 to 0.25.8
Bumps [image](https://github.com/image-rs/image) from 0.25.6 to 0.25.8.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.6...v0.25.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:04:06 +00:00
dependabot[bot]
362328167c chore(cargo): bump serde_json from 1.0.143 to 1.0.145
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.143 to 1.0.145.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.143...v1.0.145)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:01:39 +00:00
link2xt
570a9993f7 chore(release): prepare for 2.16.0 2025-10-01 13:34:45 +00:00
Hocuri
5adc68cf0b fix: Don't enable legacy decryption options
Follow-up to https://github.com/chatmail/core/pull/7226/; I believe that
this was enabled by accident? Previously, we had `allow_legacy: false`.
2025-10-01 12:29:26 +00:00
WofWca
1b1757ebf2 api: add chat_id to all call events (#7216) 2025-10-01 10:42:33 +00:00
link2xt
d8950fb7d1 fix: use nine.testrun.org as a default STUN server 2025-09-30 22:45:35 +00:00
iequidoo
ba2e573c23 fix: Allow Exif for stickers, don't recode them because of that (#6447)
Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming from UIs shouldn't
contain sensitive Exif info.
2025-09-30 01:09:14 -03:00
iequidoo
31391fc074 feat: Set dimensions for outgoing Sticker messages
For incoming `Sticker` messages, dimensions are already set, so make the code consistent.
2025-09-30 01:09:14 -03:00
iequidoo
f94b2c3794 feat: Don't ignore receive_imf_inner() errors, try adding partially downloaded message instead (#7196)
Ignoring `receive_imf_inner()` errors, i.e. silently skipping messages on failures, leads to bugs
never fixed. As for temporary I/O errors, ignoring them leads to lost messages, in this case it's
better to bubble up the error and get the IMAP loop stuck. However if there's some logic error, it's
better to show it to the user so that it's more likely reported, and continue receiving messages. To
distinguish these cases, on error, try adding the message as partially downloaded with the error set
to `msgs.error`, this way the user also can retry downloading the message to finally see it if the
problem is fixed.
2025-09-30 00:54:24 -03:00
iequidoo
eb0a5fed8e fix: receive_imf: Report replaced message id in MsgsChanged if chat is the same 2025-09-30 00:54:24 -03:00
link2xt
eaa47d175f feat: get ICE servers from IMAP METADATA 2025-09-28 02:06:43 +00:00
link2xt
e968000a89 api(jsonrpc): add has_video attribute to call info 2025-09-27 18:24:39 +00:00
link2xt
1ba448fe19 api(jsonrpc): add state attribute to call info
Existing boolean attributes removed.
2025-09-26 17:34:54 +00:00
link2xt
a5c82425f4 fix: do not consider the call stale if it is not sent out yet 2025-09-26 17:34:54 +00:00
link2xt
1bd31f6b8e api: add CallState 2025-09-26 17:34:54 +00:00
link2xt
c0ea0e52b3 test: do not match call ID from second alice with first alice event 2025-09-26 17:34:54 +00:00
link2xt
e6a3daacb3 test: update timestamp_sent in pop_sent_msg_opt()
Otherwise "sent" messages in tests have 0 timestamp.
2025-09-26 17:34:54 +00:00
link2xt
09dabda4a3 build: update rPGP from 0.16.0 to 0.17.0 2025-09-26 16:54:49 +00:00
link2xt
f523d912af ci: install rustfmt before checking provider database
Apparently it is not installed by default anymore.
2025-09-26 00:02:47 +00:00
missytake
90b0ca79ea api(deltachat-rpc-client): add Chat.resend_messages() 2025-09-25 08:19:14 +02:00
link2xt
a506e2d5a2 api: add chat ID to SecureJoinInviterProgress 2025-09-23 23:23:21 +00:00
link2xt
4c66518a68 docs: SecurejoinInviterProgress never returns an error 2025-09-23 23:23:21 +00:00
iequidoo
42b4b83f8e fix: Don't add "member removed" messages from nonmembers (#7207)
Such messages are actually ignored. Showing them to users creates confusion.
`ChatGroupMemberAdded` branch is modified for code consistency.
2025-09-23 05:57:57 -03:00
iequidoo
7477ebbdd7 fix: Take the last valid Autocrypt header (#7167)
DKIM-Signatures apply to the last headers, so start from the last header and take a valid one,
i.e. skip headers having unknown critical attributes, etc. Though this means that Autocrypt header
must be "oversigned" to guarantee that a not DKIM-signed header isn't taken, still start from the
last header for consistency with processing other headers. This isn't a security issue anyway.
2025-09-22 15:38:32 -03:00
link2xt
738dc5ce19 api: add call_info() JSON-RPC API 2025-09-20 18:47:47 +00:00
WofWca
3680467e14 fix: don't init Iroh on channel leave (#7210)
Some Delta Chat clients (Desktop, for example)
do `leave_webxdc_realtime`
regardless of whether we've ever joined a realtime channel
in the first place. Such as when closing a WebXDC window.
This might result in unexpected and suspicious firewall warnings.

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-09-20 04:01:09 +00:00
link2xt
c5ada9b203 api: add JSON-RPC API to get ICE servers
Currently with a hardcoded TURN server
so it can be used in the UIs.
2025-09-18 18:35:40 +00:00
link2xt
3d2805bc78 ci: update Rust to 1.90.0 2025-09-18 15:49:59 +00:00
iequidoo
2dde286d68 refactor: Remove unused FolderMeaning::Drafts 2025-09-18 00:59:18 -03:00
iequidoo
2260156c40 feat: Don't fetch messages from unknown folders (#7190)
Actually this leads to fetching messages only from watched folders and Spam. Motivation:
- At least Gmail has virtual folders which aren't correctly detected as such, e.g. "Sent".
- At least Gmail has many virtual folders and scanning all of them takes significant time, 5-6 secs
  in median for me. This slows down receiving new messages and consumes battery.
- Delta Chat shouldn't fetch messages from folders potentially created by other apps for their own
  purposes. NB: All compatible Delta Chat forks should use the "DeltaChat" folder as mvbox.
- Fetching from folders that aren't watched, e.g. from "Sent", may lead to message ordering issues.
2025-09-18 00:59:18 -03:00
link2xt
129e970727 api: add has_video attribute to incoming call events
This allows UI to show if incoming call is a video or audio call
and disable camera by default for audio calls.
2025-09-17 19:34:14 +00:00
link2xt
66271db8c0 test: rename test_udpate_call_text into test_update_call_text 2025-09-17 19:34:14 +00:00
WofWca
09d33e62bd refactor: remove unused prop (TS, BaseDeltaChat)
Apparently it has been unused ever since the introduction
of JSON-RPC, 0887acf1bf
(https://github.com/chatmail/core/pull/3463).
2025-09-17 17:40:15 +00:00
WofWca
bf3dfa4ab6 docs: add more get_next_event docs 2025-09-17 15:47:50 +00:00
link2xt
40b866117e fix: ignore vc-/vg- prefix for SecurejoinInviterProgress
Inviter progress is for group if we added Bob to the group,
not if Bob sent us vg-request-with-auth.
2025-09-16 18:00:15 +00:00
link2xt
cb5f9f3051 api!: get rid of inviter progress other than 0 and 1000
UIs don't display a dialog with a progress bar anyway.
2025-09-16 18:00:15 +00:00
link2xt
80f97cf9bd fix: create 1:1 chat only if auth token is for setup contact
Previously we trusted Bob to send the correct vc- or vg- prefix.
2025-09-16 18:00:15 +00:00
link2xt
6d860f7eae chore(release): prepare for 2.15.0 2025-09-15 15:28:41 +00:00
link2xt
545643b610 build: remove unused quoted_printable dependency 2025-09-15 11:52:31 +00:00
l
7ee6f2c36a api: add JSON-RPC API for calls (#7194) 2025-09-13 02:56:51 +00:00
link2xt
5d9b887624 chore(release): prepare for 2.14.0 2025-09-12 06:06:36 +00:00
link2xt
12c0e298f5 test: test sending SDP offer and answer with newlines 2025-09-12 02:37:57 +00:00
link2xt
f9aec7af0d fix: B-encode SDP offer and answer sent in headers
SDP offer and answer contain newlines.
Without the fix these newlines are not encoded at all
and break the header into multiple headers
or even prevent parsing of the following headers.
2025-09-12 02:37:57 +00:00
link2xt
b181d78dd5 fix(param): split params only on \n
str.lines() splits on both \n and \r\n
We use \n as a field separator,
so \r\n should not separate the fields.
2025-09-12 02:37:57 +00:00
iequidoo
b9ff40c6b5 test: Message is OutFailed if all keys are missing (#6849)
Follow-up to 143ba6d5e7 before which a message remained in
`OutPending` indefinitely in such a case.
2025-09-09 09:48:12 -03:00
iequidoo
0684810d38 refactor: prepare_msg_raw(): don't return MsgId
The function takes `&mut Message` and updates its `id` and `chat_id` before return.
2025-09-09 09:48:12 -03:00
Hocuri
1cc7ce6e27 api: Put the chattype into the SecurejoinInviterProgress event (#7181)
Quoting @adbenitez:

> I have been using the SecurejoinInviterProgress event to show a
welcome message when user scan the QR/link of the bot (== starts a chat
with the bot)

> but this have a big problem: in that event all you know is that a
contact completed the secure-join process, you don't know if it was via
certain 1:1 invite link or a group invitation, then a group-invite bot
would send you a help message in 1:1 every time you join a group with it

Since it's easy enough to add this information to the
SecurejoinInviterProgress event, I wrote a PR to do so.
2025-09-09 08:17:53 +00:00
link2xt
82bc1bf0b1 refactor: use recv_msg_trash() instead of recv_msg_opt() 2025-09-09 06:08:15 +00:00
link2xt
75bcf8660b chore(release): prepare for 2.13.0 2025-09-09 05:46:13 +00:00
dependabot[bot]
5e1d945198 chore(deps): bump actions/setup-node from 4 to 5
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-09 02:11:35 +00:00
dependabot[bot]
e047184ede chore(deps): bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-09 02:11:26 +00:00
link2xt
307a2eb6ec feat: withdraw all QR codes when one is withdrawn
This is a preparation for expiring authentication tokens.

If we make authentication token expire,
we need to generate new authentication tokens each time
QR code screen is opened in the UI,
so authentication token is fresh.
We however don't want to completely invalidate
old authentication codes at the same time,
e.g. they should still be valid for joining groups,
just not result in a verification on the inviter side.

Since a group now can have a lot of authentication tokens,
it is easy to lose track of them
without any way to remove them
as they are not displayed anywhere in the UI.
As a solution, we now remove all
tokens corresponding to a group ID
when one token is withdrawn,
or all non-group tokens
when a single non-group token is withdrawn.

"Reset QR code" option already present
in the UI which works by resetting
current QR code will work without any UI changes,
but will now result in invalidation
of all previously created QR codes and invite links.
2025-09-08 17:51:45 +00:00
bjoern
ab8aedf06e refine call states (#7179)
- sync declined calls from callee to caller, as usual in all larger
messengers
- introduce the call states "Missed call", "Declined call" and
"Cancelled all" ("Ended call" is gone)
- allow calling end_call()/accept_call() for already ended/accepted
calls, in practise, handling all cornercases is tricky in UI - and the
state needs anyways to be tracked.
- track and show the call duration

the duration calculation depends on local time, but it is displayed only
coarse and is not needed for any state. this can be improved as needed,
timestamps of the corresponding messages are probably better at some
point. or ending device sends its view of the time around. but for the
first throw, it seems good enough

if we finally want that set of states, it can be exposed to a json-info
in a subsequent call, so that the UI can render it more nicely. fallback
strings as follows will stay for now to make adaption in other UI easy,
and for debugging:

<img width="320" alt="IMG_0154"
src="https://github.com/user-attachments/assets/09a89bfb-66f4-4184-b05c-e8040b96cf44"
/>

successor of https://github.com/chatmail/core/pull/6650
2025-09-08 15:48:35 +02:00
bjoern
b6ab13f1de feat: hide call status change messages (#7175)
this PR uses the initial "call messages" (that has a separate viewtype
since #7174) to show all call status.

this is what most other messengers are doing as well. additional "info
messages" after a call are no longer needed.

on the wire, as we cannot pickpack on visible info messages, we use
hidden messages, similar to eg. webxdc status updates.

in future PR, it is planned to allow getting call state as a json, so
that UI can render nicely. it is then decided if we want to translate
the strings in the core.

<img width="320" alt="IMG_0150"
src="https://github.com/user-attachments/assets/41ee3fa3-8be4-42c3-8dd9-d20f49881650"
/>

successor of https://github.com/chatmail/core/pull/6650
2025-09-05 08:52:15 +02:00
link2xt
53a3e51920 feat: support receiving Autocrypt-Gossip with _verified attribute
This commit is a preparation for
sending Autocrypt-Gossip with `_verified` attribute
instead of `Chat-Verified` header.
2025-09-04 19:46:14 +00:00
link2xt
4033566b4a refactor: remove Aheader::new 2025-09-04 19:46:14 +00:00
bjoern
bed1623dcb feat: use dedicated 'call' viewtype (#7174)
a dedicated viewtype allows the UI to show a more advanced UI, but even
when using the defaults,
it has the advantage that incoming/outgoing and the date are directly
visible.

successor of https://github.com/chatmail/core/pull/6650
2025-09-04 16:51:51 +02:00
link2xt
d4704977bc api!: remove e2ee_enabled preference
The setting is already removed from the UIs,
but users who had it disabled previously have
no way to enable it. After this change
encryption is effectively always preferred.
2025-09-04 13:58:05 +00:00
link2xt
838eed94bc chore: update provider database 2025-09-04 13:58:05 +00:00
link2xt
9870725d1f refactor: remove unused EncryptPreference::Reset 2025-09-04 13:58:05 +00:00
link2xt
ba827283be docs(STYLE.md): prefer BTreeMap and BTreeSet over hash variants 2025-09-04 12:26:50 +00:00
dependabot[bot]
1e37cb8c3c chore(cargo): bump nu-ansi-term from 0.46.0 to 0.50.1
Bumps [nu-ansi-term](https://github.com/nushell/nu-ansi-term) from 0.46.0 to 0.50.1.
- [Release notes](https://github.com/nushell/nu-ansi-term/releases)
- [Changelog](https://github.com/nushell/nu-ansi-term/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nushell/nu-ansi-term/compare/v0.46.0...v0.50.1)

---
updated-dependencies:
- dependency-name: nu-ansi-term
  dependency-version: 0.50.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-03 13:17:03 -03:00
dependabot[bot]
1991e01641 Merge pull request #7157 from chatmail/dependabot/cargo/tempfile-3.21.0 2025-09-02 23:29:24 +00:00
dependabot[bot]
d7e87b6336 Merge pull request #7152 from chatmail/dependabot/cargo/syn-2.0.106 2025-09-02 23:18:00 +00:00
dependabot[bot]
fde490ba15 chore(cargo): bump tempfile from 3.20.0 to 3.21.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.20.0 to 3.21.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.20.0...v3.21.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 22:01:42 +00:00
dependabot[bot]
cf5a16d967 chore(cargo): bump syn from 2.0.104 to 2.0.106
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.104 to 2.0.106.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.104...2.0.106)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 22:01:28 +00:00
dependabot[bot]
e8dde9c63d chore(cargo): bump thiserror from 2.0.12 to 2.0.16
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 2.0.12 to 2.0.16.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.12...2.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 22:00:03 +00:00
dependabot[bot]
667a935665 chore(cargo): bump serde_json from 1.0.142 to 1.0.143
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.142 to 1.0.143.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.142...v1.0.143)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 21:34:37 +00:00
dependabot[bot]
28cea706fa chore(cargo): bump anyhow from 1.0.98 to 1.0.99
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.98 to 1.0.99.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.98...1.0.99)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 21:20:26 +00:00
dependabot[bot]
209a990444 chore(cargo): bump libc from 0.2.174 to 0.2.175
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.174 to 0.2.175.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.175/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.174...0.2.175)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 21:19:06 +00:00
dependabot[bot]
6365a46fac chore(cargo): bump percent-encoding from 2.3.1 to 2.3.2
Bumps [percent-encoding](https://github.com/servo/rust-url) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/commits)

---
updated-dependencies:
- dependency-name: percent-encoding
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 21:18:44 +00:00
dependabot[bot]
a81496e9ab Merge pull request #7150 from chatmail/dependabot/cargo/quick-xml-0.38.3 2025-09-02 18:56:21 +00:00
dependabot[bot]
ca05733b9d Merge pull request #7151 from chatmail/dependabot/cargo/toml-0.9.5 2025-09-02 18:55:42 +00:00
dependabot[bot]
dfb5348a78 Merge pull request #7156 from chatmail/dependabot/cargo/brotli-8.0.2 2025-09-02 18:55:16 +00:00
dependabot[bot]
602e52490c Merge pull request #7158 from chatmail/dependabot/cargo/async_zip-0.0.18 2025-09-02 18:54:53 +00:00
dependabot[bot]
740b24e8a4 Merge pull request #7160 from chatmail/dependabot/cargo/futures-lite-2.6.1 2025-09-02 18:53:52 +00:00
dependabot[bot]
44a09ffd12 Merge pull request #7164 from chatmail/dependabot/cargo/hyper-1.7.0 2025-09-02 18:49:24 +00:00
dependabot[bot]
054c42cbc2 Merge pull request #7159 from chatmail/dependabot/cargo/uuid-1.18.0 2025-09-02 18:48:37 +00:00
dependabot[bot]
34263a70e2 Merge pull request #7162 from chatmail/dependabot/cargo/tokio-util-0.7.16 2025-09-02 18:48:05 +00:00
link2xt
7ea6ca35d7 feat: do not replace messages with an error on verification failure 2025-09-02 18:29:53 +00:00
link2xt
a9aad497fc api!: remove deprecated is_protection_broken 2025-09-02 18:29:53 +00:00
link2xt
7da8489635 api!: remove is_profile_verified APIs
UIs now display green checkmark in a profile
if the contact is verified.
Chats with key-contacts cannot become unprotected,
so there is no need to check 1:1 chat.
2025-09-02 18:29:53 +00:00
link2xt
683561374d test: add TestContext.create_chat_id 2025-09-02 18:29:53 +00:00
link2xt
66c9982822 fix: add "Messages are end-to-end encrypted." to non-protected groups
The messages are end-to-end encrypted
in encrypted group regardless
of whether the group is protected or not.
2025-09-02 18:29:53 +00:00
link2xt
1b6450b210 feat: do not set "unknown sender for this chat" error 2025-09-02 18:29:53 +00:00
dependabot[bot]
aa8a13adb2 chore(cargo): bump hyper from 1.6.0 to 1.7.0
Bumps [hyper](https://github.com/hyperium/hyper) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: hyper
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 17:51:13 +00:00
dependabot[bot]
5888541c05 chore(cargo): bump tokio-util from 0.7.14 to 0.7.16
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.14 to 0.7.16.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.14...tokio-util-0.7.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 17:08:06 +00:00
dependabot[bot]
f893487dc0 chore(cargo): bump futures-lite from 2.6.0 to 2.6.1
Bumps [futures-lite](https://github.com/smol-rs/futures-lite) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/smol-rs/futures-lite/releases)
- [Changelog](https://github.com/smol-rs/futures-lite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/futures-lite/compare/v2.6.0...v2.6.1)

---
updated-dependencies:
- dependency-name: futures-lite
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 17:02:54 +00:00
dependabot[bot]
b84beaf974 chore(cargo): bump uuid from 1.17.0 to 1.18.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.17.0...v1.18.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 16:37:29 +00:00
dependabot[bot]
75a3c55e70 chore(cargo): bump async_zip from 0.0.17 to 0.0.18
Bumps [async_zip](https://github.com/Majored/rs-async-zip) from 0.0.17 to 0.0.18.
- [Commits](https://github.com/Majored/rs-async-zip/compare/v0.0.17...v0.0.18)

---
updated-dependencies:
- dependency-name: async_zip
  dependency-version: 0.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 15:57:03 +00:00
dependabot[bot]
854a09e12f chore(cargo): bump brotli from 8.0.1 to 8.0.2
Bumps [brotli](https://github.com/dropbox/rust-brotli) from 8.0.1 to 8.0.2.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits/8.0.2)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 15:40:32 +00:00
dependabot[bot]
40412fd4a9 chore(cargo): bump toml from 0.9.4 to 0.9.5
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.4 to 0.9.5.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.4...toml-v0.9.5)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 14:32:01 +00:00
dependabot[bot]
57fc084795 chore(cargo): bump quick-xml from 0.37.5 to 0.38.3
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.37.5 to 0.38.3.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.37.5...v0.38.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 14:25:16 +00:00
Nico de Haen
143ba6d5e7 fix: Mark message as failed if it can't be send (#7143)
resolves #6849
2025-09-02 14:32:25 +02:00
bjoern
6b338a923c feat: warn for outdated versions after 6 months (#7144)
3 months were proven to be too short some years ago, after that issue,
we went far up to 12 months.
however, 12 months were considered too long after recent discussions :)
so, 6 months seems to be a good compromise.

the warning is still repeated every months and the text is unchanged.

advantage is still that this approach does not require network or
opt-in, and catches really all lazy updaters with few effort, cmp
https://github.com/deltachat/deltachat-desktop/issues/5422
2025-09-01 17:44:54 +02:00
iequidoo
e6ab1e3df5 fix: Update verifier_id if it's "unknown" and new verifier has known verifier
Now that the previous commit avoids creating incorrect reverse verification chains, we can do
this. Sure, existing users' dbs aready have verification chains ending with "unknown" roots, but at
least for new users updating `verifier_id` to a known verifier makes sense.
2025-09-01 05:09:19 -03:00
iequidoo
5da6976bf9 fix: Don't verify contacts by others having an unknown verifier
If this happens, mark the contact as verified by an unknown contact instead. This avoids introducing
incorrect reverse chains: if the verifier itself has an unknown verifier, it may be `contact_id`
actually (directly or indirectly) on the other device (which is needed for getting "verified by
unknown contact" in the first place).
2025-09-01 05:09:19 -03:00
iequidoo
bd15d90e77 refactor: Check that verifier is verified in turn 2025-09-01 05:09:19 -03:00
iequidoo
61633cf23b fix: Don't reverify contacts by SELF on receipt of a message from another device
Also verify not yet verified contacts w/o setting a verifier for them (in the db it's stored as
`verifier_id=id` though) because we don't know who verified them for another device.
2025-09-01 05:09:19 -03:00
iequidoo
9f1107c0e7 docs: Fix for SecurejoinInviterProgress with progress == 600 2025-09-01 03:57:51 -03:00
Hocuri
ff0d5ce179 test: Add another TimeShiftFalsePositiveNote (#7142)
test_maybe_warn_on_outdated() can also fail when run with `cargo test`,
rather than `cargo nextest` (just happened to me)
2025-08-31 19:32:30 +00:00
bjoern
0bbd910883 feat: add call ringing API (#6650)
this PR adds a "ringing" api that can be used for calls later.

see deltachat.h for details about the API; jsonrpc is left out until
things are settled for the needs of android/iOS

UI using this PR already successfully are
https://github.com/deltachat/deltachat-ios/pull/2638 and
https://github.com/deltachat/deltachat-android/pull/3785 ; the "payload"
passed forth and back is optimised for
https://github.com/deltachat/calls-webapp

---------

Co-authored-by: l <link2xt@testrun.org>
2025-08-30 23:48:38 +02:00
dependabot[bot]
4258088fb4 chore(cargo): bump tracing-subscriber from 0.3.19 to 0.3.20
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.19 to 0.3.20.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.20
  dependency-type: direct:production
...

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-08-30 14:21:08 -03:00
link2xt
6372b677d2 chore(release): prepare for 2.12.0 2025-08-26 21:22:39 +00:00
link2xt
9af00af70f docs: remove the comment about Color Vision Deficiency correction
Color Vision Deficiency correction has been removed from https://xmpp.org/extensions/xep-0392.html
in version 0.8.0, see the reasoning there.
2025-08-26 21:14:08 +00:00
link2xt
4010c60e7b feat: use key fingerprints for color generation
This way contact colors stays the same
even if the address changes later.
2025-08-26 21:14:08 +00:00
link2xt
aaa83a8f52 feat: replace HSLuv colors with OKLCh 2025-08-26 21:14:08 +00:00
link2xt
776408c564 fix: do not create a group if the sender includes self in the To field 2025-08-26 18:06:17 +00:00
iequidoo
d0cb2110e6 feat: Chat::get_color(): Use grpid, if present, instead of name
While testing the previous commit i understood that it's better to try giving different colors to
groups, particularly if their names are equal so that they visually differ, and at the same time
preserve the color if the group is renamed. Using `grpid` solves this. So let groups change colors
once and forever.
2025-08-24 12:10:54 -03:00
iequidoo
11e3480fe8 feat: create_group_ex(): Log and replace invalid chat name with "…"
We can't just fail on an invalid chat name because the user would lose the work already done in the
UI like selecting members. Sometimes happens to me when i put space into name.
2025-08-24 12:10:54 -03:00
Hocuri
2cd54b72b0 refactor: Make ConnectivityStore use a non-async lock (#7129)
Follow-up to https://github.com/chatmail/core/pull/7125: We now have a
mix of non-async (parking_lot) and async (tokio) Mutexes used for the
connectivity. We can just use non-async Mutexes, because we don't
attempt to hold them over an await point. I also tested that we get a
compiler error if we do try to hold one over an await point (rather than
just deadlocking/blocking the executor on runtime).

Not 100% sure about using the parking_lot rather than std Mutex, because
since https://github.com/rust-lang/rust/issues/93740, parking_lot
doesn't have a lot of advantages anymore. But as long as iroh depends on
it, we might as well use it ourselves.
2025-08-23 21:08:17 +02:00
iequidoo
c34ccafb2e fix: Make reaction message hidden only if there are no other parts
RFC 9078 "Reaction: ..." doesn't forbid messages with reactions to have other parts, so be prepared
for this.
2025-08-22 17:32:55 -03:00
iequidoo
6837874d43 fix: get_connectivity(): Get rid of locking SchedulerState::inner (#7124)
`get_connectivity()` is expected to return immediately, not when the scheduler finishes updating its
state in `start_io()/stop_io()/pause_io()`, otherwise it causes app non-responsiveness.

Instead of read-locking `SchedulerState::inner`, store the `ConnectivityStore` collection in
`Context` and fetch it from there in `get_connectivity()`. Update it every time we release a write
lock on `SchedulerState::inner`.
2025-08-22 14:18:30 -03:00
link2xt
3656337d41 refactor: do not return Result from valid_signature_fingerprints()
This function never fails.
2025-08-18 07:47:34 +00:00
link2xt
a89b6321f1 feat: assign messages to key-contacts based on Issuer Fingerprint 2025-08-16 23:22:35 +00:00
link2xt
ac10103b18 api!(python): remove remaining broken API for reactions
CFFI for reactions has been previously deprecated and removed,
so legacy python bindings using it don't work anymore.
2025-08-16 19:50:04 +00:00
iequidoo
b696a242fc feat: wal_checkpoint(): Do wal_checkpoint(PASSIVE) and wal_checkpoint(FULL) before wal_checkpoint(TRUNCATE)
This way the subsequent `wal_checkpoint(TRUNCATE)` is faster. We don't want to block writers and
readers for a long period.
2025-08-15 14:00:41 -03:00
link2xt
7e4822c8ca fix: do not reverify already verified contacts via gossip
If the contact is already introduced by someone,
usually by adding to a verified group,
it should not be reverified because of another
chat message is a verified group.
This usually results is verification loops
and is not meaningful because the verifier
likely got this same contact introduced
in the same group.
2025-08-14 15:19:09 +00:00
link2xt
a955cb5400 docs: remove broken link from documentation comments
There are many servers by now so it is not
just a workaround for one server or location.
Stated reason is clear enough without
pointing to an issue.
2025-08-13 07:03:45 +00:00
link2xt
2e2cfc4cb3 chore(release): prepare for 2.11.0 2025-08-13 00:40:18 +00:00
iequidoo
4157d1986f fix: Add messages that can't be verified as DownloadState::Available (#7059)
We haven't dropped verified groups yet, so we need to do smth with messages that can't be verified
yet which often occurs because of messages reordering, particularly in large groups. Apart from the
reported case #7059, i had other direct reports that sometimes messages can't be verified for
various reasons, but when the reason is already fixed, it should be possible to re-download failed
messages and see them.

Also remove the code replacing the message text with a verification error from
`apply_group_changes()` as `add_parts()` already does this.
2025-08-12 20:52:14 -03:00
iequidoo
d13eb2f580 feat: receive_imf::add_parts(): Get rid of extra Chat::load_from_db() calls 2025-08-12 20:52:14 -03:00
link2xt
5476f69179 fix: don't break long group names with non-ASCII characters
The fix is in mail-builder 0.4.4.
2025-08-12 23:50:58 +00:00
dependabot[bot]
dcdf30da35 Merge pull request #7103 from chatmail/dependabot/github_actions/actions/download-artifact-5 2025-08-12 23:00:40 +00:00
dependabot[bot]
55746c8c19 Merge pull request #7104 from chatmail/dependabot/github_actions/actions/checkout-5 2025-08-12 23:00:03 +00:00
iequidoo
dbdf5f2746 feat: get_securejoin_qr(): Log error if group doesn't have grpid
This doesn't fix anything in UIs currently because they don't call `get_securejoin_qr()` for
unencrypted groups, but it's still better to log an error which will be shown in this case.
2025-08-12 19:59:00 -03:00
iequidoo
b4e28deed3 feat: lookup_key_contact_by_address(): Allow looking up ContactId::SELF without chat id
This doesn't fix anything currently, but let's allow such lookups to avoid future bugs.
2025-08-12 19:59:00 -03:00
iequidoo
f4a604dcfb refactor: Chat::is_encrypted(): Make one query instead of two for 1:1 chats 2025-08-12 19:59:00 -03:00
Hocuri
b3c5787ec8 test: Log the number of the test account if there are multiple alices (#7087)
In order to debug some test failures wrt broadcast channels, I need to
read the log of some tests that have an alice0 and an alice1.

It's currently not possible to tell whether a line was logged by alice0
or alice1, so, I would like to change that with this PR.

Edit: Turns out that there are more tests that call their profiles
"alice1"/"alice2" (or "alice"/"alice2") than "alice0"/"alice1". So, I
changed the logging to count "alice", "alice2", "alice3", ….

Not sure whether I should adapt old tests; it might save someone some
confusion in the future, but I might also make a mistake and make it so
that the test doesn't properly test anymore.
2025-08-12 08:51:23 +00:00
dependabot[bot]
471d0469dd chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 08:21:41 +00:00
dependabot[bot]
113eda575f chore(deps): bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 05:32:37 +00:00
link2xt
45f1da82fe fix: take Chat-Group-Name into account when matching ad hoc groups 2025-08-12 01:10:49 +00:00
link2xt
5f45ff77e4 chore: typo fix 2025-08-12 01:10:49 +00:00
link2xt
1c0201ee3d fix: assign messages to a group if there is a Chat-Group-Name
Otherwise messages sent to small groups
with only two members are incorrectly assigned
to 1:1 chat.
2025-08-12 01:10:49 +00:00
link2xt
c7340e04ec feat: do not require resent messages to be from the same chat
Chat was only loaded to avoid removing GuaranteeE2ee
for protected chats, but resending a message
in protected chat is guaranteed to be encrypted anyway.
2025-08-11 20:11:43 +00:00
link2xt
0a32476dc5 fix: do not reset GuaranteeE2ee in the database when resending messages
Otherwise if the message is loaded by the UI
after GuaranteeE2ee is reset but before SMTP queue item
is created, the message may appear as unencrypted
even if it was actually resent as encrypted.
2025-08-11 20:11:43 +00:00
link2xt
e02bc6ffb5 ci: update Rust to 1.89.0 2025-08-11 19:40:07 +00:00
link2xt
f41a3970b2 fix: do not add key-contacts to unencrypted groups
Encrypted message may create unencrypted groups
if the message does not have a Chat-Group-ID.
This can happen if v1 client sends an encrypted
message to opportunistically encrypted ad hoc group.
In this case `from_id` corresponds to the key-contact,
but we should add address-contact of the sender
to the member list.
2025-08-11 10:33:27 +00:00
bjoern
6c536f3a9b fix: log and set imex progress error (#7091)
IMEX_PROGRESS(0) event is fired in case of errors, however, the last
error was not set in this case.

this is similar to the fix at #4195
and improves the error shown in the dialog for android and iOS; desktop
does not show an error dialog at all.

<img width="320" alt="IMG_9995"
src="https://github.com/user-attachments/assets/7065fc3d-3f30-4691-b1b2-1950564a25e2"
/>

relates to https://github.com/deltachat/deltachat-android/issues/3533
2025-08-09 01:17:27 +02:00
link2xt
4b24b6a848 refactor: skip loading the contact of 1:1 unencrypted chat to show the avatar
If the chat is unencrypted, it gets unencrypted icon.
There is no need to load the address-contact to get the same icon.
2025-08-08 19:08:23 +00:00
link2xt
5f254a929f refactor: rename icon-address-contact to icon-unencrypted
The icon is mainly used to identify unencrypted chats
in the chatlist where encrypted and unencrypted chats are mixed.
It is used for group chats rather than only for 1:1 chats
with address-contacts.
2025-08-08 19:08:23 +00:00
B. Petersen
8df1a01ace feat: better string when using disappearing messages of one year (365..367 days, so it can be tweaked later) 2025-08-08 18:45:35 +00:00
link2xt
27b5ffb34f fix: do not remove query parameters from URLs
The fix is similar to the one done in
4ca0ce2fb2,
but now extended to requests other than empty POST requests as well.
2025-08-07 14:49:18 +00:00
link2xt
80af012962 fix: set correct sent_timestamp for saved outgoing messages 2025-08-07 07:58:53 +00:00
iequidoo
615c80bef4 feat: Contact::lookup_id_by_addr_ex: Prefer returning accepted contacts
We don't want to prefer returning verified contacts because e.g. if a bot was reinstalled and its
key changed, it may not be verified, and we don't want to bring the user to the old chat if they
click on the bot email address. But trying to return accepted contacts increases security and
doesn't break the described scenario.
2025-08-06 17:37:17 -03:00
iequidoo
f5f4026dbb feat: Contact::lookup_id_by_addr_ex: Prefer returning key-contact
If an address-contact and a key-contact were seen at exactly the same time, that doesn't necessarily
mean that it's a random event, it might occur because some code updates contacts this way in some
scenario. While this is unlikely, prefer to look up the key-contact.
2025-08-06 17:37:17 -03:00
link2xt
b431206ede fix: allow receiving empty files 2025-08-06 13:33:04 +00:00
iequidoo
c4878e9b49 fix: Run wal_checkpoint during housekeeping (#6089)
Work around possible checkpoint starvations (there were cases reported when a WAL file is bigger
than 200M) and also make sure we truncate the WAL periodically. Auto-checkponting does not normally
truncate the WAL (unless the `journal_size_limit` pragma is set), see
https://www.sqlite.org/wal.html.
2025-08-05 14:58:32 -03:00
B. Petersen
aa452971a6 fix: ignore case when trying to detect 'invalid unencrypted mail' and add an info-message 2025-08-05 15:58:43 +02:00
dependabot[bot]
2d798f7cfe Merge pull request #7066 from chatmail/dependabot/cargo/toml-0.9.4 2025-08-04 23:53:05 +00:00
link2xt
08bb0484eb chore(release): prepare for 2.10.0 2025-08-04 22:33:59 +00:00
link2xt
b0b7337f5a chore: upgrade async-imap to 0.11.1 2025-08-04 21:42:28 +00:00
Hocuri
93241a4beb feat: Also lookup key contacts in lookup_id_by_addr() (#7073)
If there is both a key and an address contact, return the most recently
seen one.
2025-08-04 21:32:09 +02:00
iequidoo
4f1bf1f13c chore(deny.toml): add exception for duplicate toml_datetime 0.6.11 dependency 2025-08-02 16:16:43 -03:00
iequidoo
2d0b7b5bd8 chore(cargo): bump human-panic from 2.0.2 to 2.0.3 2025-08-02 16:14:48 -03:00
dependabot[bot]
8fe3ce5cab chore(cargo): bump strum_macros from 0.27.1 to 0.27.2
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.27.1 to 0.27.2.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/compare/v0.27.1...v0.27.2)

---
updated-dependencies:
- dependency-name: strum_macros
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 14:15:55 -03:00
dependabot[bot]
59a0f1d94f chore(cargo): bump strum from 0.27.1 to 0.27.2
Bumps [strum](https://github.com/Peternator7/strum) from 0.27.1 to 0.27.2.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/compare/v0.27.1...v0.27.2)

---
updated-dependencies:
- dependency-name: strum
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:55:09 -03:00
dependabot[bot]
5175dc3450 chore(cargo): bump criterion from 0.6.0 to 0.7.0
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.6.0 to 0.7.0.
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.6.0...0.7.0)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:39:05 -03:00
dependabot[bot]
9a22ccd058 chore(cargo): bump hyper-util from 0.1.14 to 0.1.16
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.14 to 0.1.16.
- [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.14...v0.1.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:38:26 -03:00
dependabot[bot]
c06ed49a2a chore(cargo): bump async-channel from 2.3.1 to 2.5.0
Bumps [async-channel](https://github.com/smol-rs/async-channel) from 2.3.1 to 2.5.0.
- [Release notes](https://github.com/smol-rs/async-channel/releases)
- [Changelog](https://github.com/smol-rs/async-channel/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-channel/compare/v2.3.1...v2.5.0)

---
updated-dependencies:
- dependency-name: async-channel
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:33:11 -03:00
dependabot[bot]
2e51a5a454 chore(cargo): bump bolero from 0.13.3 to 0.13.4
Bumps [bolero](https://github.com/camshaft/bolero) from 0.13.3 to 0.13.4.
- [Changelog](https://github.com/camshaft/bolero/blob/master/CHANGELOG.md)
- [Commits](https://github.com/camshaft/bolero/commits)

---
updated-dependencies:
- dependency-name: bolero
  dependency-version: 0.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 12:59:56 -03:00
dependabot[bot]
75cc353528 chore(cargo): bump serde_json from 1.0.140 to 1.0.142
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.140 to 1.0.142.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.140...v1.0.142)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 12:51:47 -03:00
dependabot[bot]
3977580426 chore(cargo): bump toml from 0.8.23 to 0.9.4
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.23 to 0.9.4.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v0.9.4)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-01 22:00:13 +00:00
link2xt
3a1370e174 chore(release): prepare for 2.9.0 2025-07-31 18:57:58 +00:00
iequidoo
c218c05b96 fix: get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker
We were reporting the UTC midnight timestamp instead. For UTC-N timezones that means reporting
"yesterday".

Fixes https://github.com/deltachat/deltachat-desktop/issues/5215.
2025-07-31 13:55:28 -03:00
iequidoo
db247d9f9a refactor: Don't call add_or_lookup_key_contacts() in advance
Its result isn't needed in all the branches.
2025-07-31 12:07:58 -03:00
iequidoo
78b7715ea6 refactor: Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts()
- It's obvious that addresses are needed to add contacts, so `_by_address_list` looks excessive.
- The function only looks up `SELF` by address.
- The name was confusing because there's also `lookup_key_contacts_by_address_list()` that actually
  looks up key contacts by addresses (not only `SELF`).
2025-07-31 12:07:58 -03:00
link2xt
ba76944d75 fix: display correct timer value for ephemeral timer changes
The timer change should not disappear,
but should display correct timer change.
2025-07-31 15:00:08 +00:00
cliffmccarthy
4a1a2122f0 feat(repl): Add import-vcard and make-vcard commands (#7048)
- Added import-vcard and make-vcard commands to deltachat-repl. These
  wrap the corresponding import_vcard() and make_vcard() operations from
  src/contact.rs. Each command takes a file path argument specifying the
  location of the vCard file to be read or written. The make-vcard command
  takes one or more IDs to write to the file.
- Documented commands in "Using the CLI client" section of README.md.

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-07-30 13:16:29 -03:00
link2xt
d80b749dec chore(release): prepare for 2.8.0 2025-07-28 19:31:43 +00:00
link2xt
039a8b7c36 fix: lookup self by address if there is no fingerprint or gossip 2025-07-28 19:18:07 +00:00
iequidoo
779f58ab16 feat: Remove ProtectionBroken, make such chats Unprotected (#7041)
Chats can't break anymore.
2025-07-28 16:01:14 -03:00
link2xt
b9183fe5eb chore(release): prepare for 2.7.0 2025-07-26 22:49:44 +00:00
Hocuri
9d342671d5 fix: Do not fail to upgrade if the verifier of a contact doesn't exist anymore (#7044)
Fix https://github.com/chatmail/core/issues/7043
2025-07-26 22:41:11 +02:00
link2xt
4e47ebd5fc test: fix flaky test_webxdc_resend 2025-07-24 17:47:19 +00:00
Hocuri
d5c418e909 feat: Put the debug/release build version into the info (#7034)
The info will look like:

```
debug_assertions=On - DO NOT RELEASE THIS BUILD
```
or:
```
debug_assertions=Off
```

I tested that this actually works when compiling on Android.

<details><summary>This is how it looked before the second
commit</summary>
<p>


The deltachat_core_version line in the info will look like:

```
deltachat_core_version=v2.5.0 [debug build]
```
or:
```
deltachat_core_version=v2.5.0 [release build]
```

I tested that this actually works when compiling on Android.



</p>
</details>
2025-07-24 17:24:54 +02:00
Hocuri
85414558c5 test: Add regression test for verification-gossiping crash (#7033)
This adds a test for https://github.com/chatmail/core/pull/7032/.

The crash happened if you received a message that has the from contact
also in the "To: " and "Chat-Group-Member-Fpr: " headers. Not sure how
it happened that such a message was created; I managed to create one in
a test, but needed to access some internals of MimeFactory for that.

I then saved the email that is sent by Alice into the test-data
directory, and then made a test that Bob doesn't crash if he receives
this email. And as a sanity-check, also test that Bob marks Fiona as
verified after receiving this email.
2025-07-24 17:24:00 +02:00
iequidoo
d6af8d2526 feat: mimefactory: Order message recipients by time of addition (#6872)
Sort recipients by `add_timestamp DESC` so that if the group is large and there are multiple SMTP
messages, a newly added member receives the member addition message earlier and has gossiped keys of
other members (otherwise the new member may receive messages from other members earlier and fail to
verify them).
2025-07-24 03:11:46 -03:00
Sebastian Klähn
1209e95e34 fix: realtime late join (#6869)
This PR adds a test to reproduce a bug raised by @adbenitez that peer
channels break when the resend feature is used.

---------

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-07-23 12:50:53 +02:00
Hocuri
51f9279e67 chore(release): prepare for 2.6.0 2025-07-23 11:47:05 +02:00
Hocuri
f27d54f7fa fix: Fix crash when receiving a verification-gossiping message which a contact also sends to itself (#7032)
This should fix https://github.com/chatmail/core/issues/7030; @r10s if
you could test whether it fixes your problem

The crash happened if you received a message that has the from contact
also in the "To: " and "Chat-Group-Member-Fpr: " headers. Not sure how
it happened that such a message was created.
2025-07-23 11:09:11 +02:00
link2xt
7f3648f8ae chore(release): prepare for 2.5.0 2025-07-22 14:21:07 +00:00
link2xt
49fc258578 fix: do not ignore errors in add_flag_finalized_with_set 2025-07-22 14:17:30 +00:00
link2xt
0c51b4fe41 docs(STYLE.md): prefer try_next() over next() 2025-07-22 12:33:37 +00:00
bjoern
dbad714539 docs: clarify the meaning of is_verified() vs verifier_id() (#7027)
this PR adapts the documentation UI guidance to recent "green checkmark"
discussions

cmp https://github.com/deltachat/deltachat-pages/pull/1145,
https://github.com/deltachat/deltachat-ios/pull/2781,
https://github.com/deltachat/deltachat-android/pull/3828,
https://github.com/deltachat/deltachat-desktop/pull/5318

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-22 10:40:12 +02:00
Hocuri
edd8008650 fix: Mark all email chats as unprotected in the migration (#7026)
Previously, chat protection was only removed if the chat became an
email-chat because the key-contact has a different email, but not if the
chat became an email-chat for a different reason.

Therefore, it could happen that the migration produced a protected,
unencrypted chat, where you couldn't write any messages.

I tested that applying this fix actually fixes the bug I had.
2025-07-21 20:15:59 +00:00
Hocuri
615a1b3f4e fix: Correctly migrate "verified by me" 2025-07-21 18:28:19 +00:00
bjoern
fe6044e1aa docs: deprecate protection-broken and related stuff (#7018)
came over these parts while targeting the new info message of
https://github.com/chatmail/core/pull/7008 in
https://github.com/deltachat/deltachat-ios/pull/2778 and
https://github.com/deltachat/deltachat-android/pull/3822

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-21 18:40:00 +02:00
207 changed files with 15560 additions and 7574 deletions

View File

@@ -7,6 +7,8 @@ updates:
commit-message:
prefix: "chore(cargo)"
open-pull-requests-limit: 50
cooldown:
default-days: 7
# Keep GitHub Actions up to date.
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
@@ -14,3 +16,5 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7

View File

@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.88.0
RUST_VERSION: 1.91.0
# Minimum Supported Rust Version
MSRV: 1.85.0
@@ -30,7 +30,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -53,7 +53,7 @@ jobs:
name: cargo deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -67,10 +67,12 @@ jobs:
name: Check provider database
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Install rustfmt
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
- name: Check provider database
run: scripts/update-provider-database.sh
@@ -80,7 +82,7 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -115,7 +117,7 @@ jobs:
shell: bash
if: matrix.rust == 'latest'
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -137,12 +139,12 @@ jobs:
- name: Tests
env:
RUST_BACKTRACE: 1
run: cargo nextest run --workspace
run: cargo nextest run --workspace --locked
- name: Doc-Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace --doc
run: cargo test --workspace --locked --doc
- name: Test cargo vendor
run: cargo vendor
@@ -154,7 +156,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -166,7 +168,7 @@ jobs:
run: cargo build -p deltachat_ffi
- name: Upload C library
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug/libdeltachat.a
@@ -179,7 +181,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -191,7 +193,7 @@ jobs:
run: cargo build -p deltachat-rpc-server
- name: Upload deltachat-rpc-server
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
@@ -201,7 +203,7 @@ jobs:
name: Python lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -226,9 +228,9 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.13
python: 3.14
- os: macos-latest
python: 3.13
python: 3.14
# PyPy tests
- os: ubuntu-latest
@@ -244,19 +246,19 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
- name: Install python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
@@ -279,11 +281,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.13
python: 3.14
- os: macos-latest
python: 3.13
python: 3.14
- os: windows-latest
python: 3.13
python: 3.14
# PyPy tests
- os: ubuntu-latest
@@ -297,13 +299,13 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Install python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
@@ -311,7 +313,7 @@ jobs:
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug

View File

@@ -30,17 +30,17 @@ jobs:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
- name: Upload binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux
path: result/bin/deltachat-rpc-server
@@ -54,17 +54,17 @@ jobs:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
- name: Upload binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}
path: result/bin/deltachat-rpc-server.exe
@@ -79,7 +79,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -91,7 +91,7 @@ jobs:
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
@@ -105,17 +105,17 @@ jobs:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
- name: Upload binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android
path: result/bin/deltachat-rpc-server
@@ -132,74 +132,74 @@ jobs:
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
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@v4
uses: actions/download-artifact@v6
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@v4
uses: actions/download-artifact@v6
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@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -224,7 +224,7 @@ jobs:
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.12
@@ -285,76 +285,76 @@ jobs:
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
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@v4
uses: actions/download-artifact@v6
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@v4
uses: actions/download-artifact@v6
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@v4
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -384,7 +384,7 @@ jobs:
ls -lah
- name: Upload to artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-npm-package
path: deltachat-rpc-server/npm-package/*.tgz
@@ -401,7 +401,7 @@ jobs:
deltachat-rpc-server/npm-package/*.tgz
# Configure Node.js for publishing.
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -14,12 +14,12 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -16,12 +16,12 @@ jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Use Node.js 18.x
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 18.x
- name: Add Rust cache

View File

@@ -5,10 +5,12 @@ on:
paths:
- flake.nix
- flake.lock
- .github/workflows/nix.yml
push:
paths:
- flake.nix
- flake.lock
- .github/workflows/nix.yml
branches:
- main
@@ -19,15 +21,12 @@ jobs:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- run: nix fmt
# Check that formatting does not change anything.
- run: git diff --exit-code
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- run: nix fmt flake.nix -- --check
build:
name: nix build
@@ -81,11 +80,11 @@ jobs:
#- deltachat-rpc-server-x86_64-android
#- deltachat-rpc-server-x86-android
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -96,14 +95,15 @@ jobs:
matrix:
installable:
- deltachat-rpc-server
- deltachat-rpc-server-x86_64-darwin
# Fails to bulid
# Fails to build
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
# - deltachat-rpc-server-aarch64-darwin
# - deltachat-rpc-server-x86_64-darwin
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- run: nix build .#${{ matrix.installable }}

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -23,7 +23,7 @@ jobs:
working-directory: deltachat-rpc-client
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: python-package-distributions
path: deltachat-rpc-client/dist/
@@ -42,7 +42,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/

View File

@@ -14,15 +14,15 @@ jobs:
name: Build REPL example
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: repl.exe
path: "result/bin/deltachat-repl.exe"

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -31,12 +31,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -50,12 +50,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
@@ -72,13 +72,13 @@ jobs:
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '18'
- name: npm install

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false

View File

@@ -14,18 +14,18 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41
- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif
category: zizmor

1
.gitignore vendored
View File

@@ -36,6 +36,7 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode
.zed
python/accounts.txt
python/all-testaccounts.txt
tmp/

View File

@@ -1,5 +1,615 @@
# Changelog
## [2.26.0] - 2025-11-11
### API-Changes
- [**breaking**] JSON-RPC: `chat_type` now contains a variant of a string enum/union. Affected places: `FullChat.chat_type`, `BasicChat.chat_type`, `ChatListItemFetchResult::ChatListItem.chat_type`, `Event:: SecurejoinInviterProgress.chat_type` and `MessageSearchResult.chat_type` ([#7285](https://github.com/chatmail/core/pull/7285))
### Features / Changes
- Error toast for "Not creating securejoin QR for old broadcast".
### Fixes
- `is_encrypted()` should be true for Saved Messages chat so messages there are editable.
- Do not return an error from `receive_imf` if we fail to add a member because we are not in chat.
- Do not add QR inviter to groups immediately.
- Do not ignore I/O errors in `BlobObject::store_from_base64`.
### Miscellaneous Tasks
- Rustfmt.
### Refactor
- imap: Move resync request from Context to Imap.
- Replace imap:: calls in migration 73 with SQL queries.
- Remove unused imports.
### Documentation
- Readme: update language binding section to avoid usage of cffi in new projects ([#7380](https://github.com/chatmail/core/pull/7380)).
- Fix Context::set_stock_translation reference.
### Tests
- Test editing saved messages.
- Remove ThreadPoolExecutor from test_wait_next_messages.
- Move test_two_group_securejoins from receive_imf to securejoin module.
- At the end of securejoin Bob has two members in a group chat.
- Bob has 0 members in the chat until securejoin finishes.
- Do not add QR inviter to groups right after scanning the code.
## [2.25.0] - 2025-11-05
### Features / Changes
- Put self-name into group invite codes ([#7398](https://github.com/chatmail/core/pull/7398)).
- Slightly nicer and shorter QR and invite codes ([#7390](https://github.com/chatmail/core/pull/7390))
### Fixes
- Add device message instead of partial message when receive_imf fails. This fixes a rare bug where the IMAP loop got stuck.
- Add info message if user tries to create a QR code for deprecated channel ([#7399](https://github.com/chatmail/core/pull/7399)).
### Miscellaneous Tasks
- deps: Bump actions/upload-artifact from 4 to 5.
- deps: Bump actions/download-artifact from 5 to 6.
- deps: Bump astral-sh/setup-uv from 7.1.0 to 7.1.2.
### Refactor
- sql: Do not expose rusqlite Error type in query_map methods.
## [2.24.0] - 2025-11-03
***Note that in v2.24.0, the IMAP loop can get stuck in rare circumstances;
use v2.23.0 or v2.25.0 instead.***
### Documentation
- Comment why spaced en dash is used to separate message Subject from text.
### Features / Changes
- [**breaking**] QR codes and symmetric encryption for broadcast channels ([#7268](https://github.com/chatmail/core/pull/7268)).
- A new QR type AskJoinBroadcast; cloning a broadcast
channel is no longer possible; manually adding a member to a broadcast
channel is no longer possible (the only way to join a channel is scanning a QR code or clicking a link)
### Refactor
- Split "transport" module out of "login_param".
## [2.23.0] - 2025-11-01
### API-Changes
- Make `dc_chat_is_protected` always return 0.
- [**breaking**] Remove public APIs to check if the chat is protected.
- [**breaking**] Remove APIs to create protected chats.
- [**breaking**] Remove Chat.is_protected().
- deltachat-rpc-client: Add Account.add_transport_from_qr() API.
- JSON-RPC: add `get_push_state` to check push notification state ([#7356](https://github.com/chatmail/core/pull/7356)).
- JSON-RPC: remove unused TypeScript constants ([#7355](https://github.com/chatmail/core/pull/7355)).
- Remove `Config::SentboxWatch` ([#7178](https://github.com/chatmail/core/pull/7178)).
- Remove `Config::ConfiguredSentboxFolder` and everything related.
### Build system
- Ignore configuration for the zed editor ([#7322](https://github.com/chatmail/core/pull/7322)).
- nix: Fix build of deltachat-rpc-server-x86_64-darwin.
- Update rand to 0.9.
- Do not install `pdbpp` in the test environment for CFFI Python bindings.
- Migrate from tokio-tar to astral-tokio-tar.
- deps: Bump actions/setup-node from 5 to 6.
- deps: Bump cachix/install-nix-action from 31.8.0 to 31.8.1.
- Fix Rust 1.91.0 lint for derivable Default.
### CI
- Pin GitHub action `astral-sh/setup-uv`.
- Set 7 days cooldown on Dependabot updates.
- Update Rust to 1.91.0.
### Documentation
- Document Autocrypt-Gossip `_verified` attribute.
### Features/Changes
Metadata reduction:
- Protect Autocrypt header.
- Anonymize OpenPGP recipients (temorarily disabled due to interoperability problems, see <https://github.com/chatmail/core/issues/7384>).
- Protect the `Date` header.
Onboarding improvements:
- Allow plain domain in `dcaccount:` scheme.
- Do not resolve MX records during configuration.
Preparation for multi-transport:
- Move the messages only from INBOX and Spam folders.
- deltachat-rpc-client: Support multiple transports in resetup_account().
Various other changes:
- Opt-in weekly sending of statistics ([#6851](https://github.com/chatmail/core/pull/6851))
- Synchronize encrypted groups creation across devices ([#7001](https://github.com/chatmail/core/pull/7001)).
- Do not send Autocrypt in MDNs.
- Do not run SecureJoin if we are already in the group.
- Show if proxy is enabled in connectivity view ([#7359](https://github.com/chatmail/core/pull/7359)).
### Fixes
- Don't ignore QR token timestamp from sync messages.
- Do not allow sync item timestamps to be in the future.
- jsonrpc: Fix `ChatListItem::is_self_in_group`.
- Delete obsolete "configured*" keys from `config` table ([#7171](https://github.com/chatmail/core/pull/7171)).
- Fix flaky tests::verified_chats::test_verified_chat_editor_reordering and receive_imf::receive_imf_tests::test_two_group_securejoins.
- Stop using `leftgrps` table.
- Stop notifying about messages in contact request chats.
### Refactor
- Remove invalid Gmail OAuth2 tokens.
- Remove ProtectionStatus.
- Rename chat::create_group_chat() to create_group().
- Remove error stock strings that are rarely used these days ([#7327](https://github.com/chatmail/core/pull/7327)).
- Jsonrpc rename change casing in names of jsonrpc structs/enums to comply with rust naming conventions. ([#7324](https://github.com/chatmail/core/pull/7324)).
- Stop using deprecated Account.configure().
- add_transport_from_qr: Do not set deprecated config values.
- sql: Change second query_map function from FnMut to FnOnce.
- sql: Add query_map_vec().
- sql: Add query_map_collect().
- Use rand::fill() instead of rand::rng().fill().
- Use SampleString.
- Remove unused call to get_credentials().
### Tests
- rpc-client: VCard color is the same as the contact color ([#7294](https://github.com/chatmail/core/pull/7294)).
- Add unique offsets to ids generated by `TestContext` to increase test correctness ([#7297](https://github.com/chatmail/core/pull/7297)).
## [2.22.0] - 2025-10-17
### Fixes
- Do not notify about incoming calls for contact requests and blocked contacts.
### Tests
- Accept the chat with the caller before accepting calls.
## [2.21.0] - 2025-10-16
### Build system
- nix: Remove unused dependencies.
### Features / Changes
- TLS 1.3 session resumption.
- REPL: Add send-sync command.
- Set `User-Agent` for tile.openstreetmap.org requests.
- Cache tile.openstreetmap.org tiles for 7 days.
### Fixes
- Remove Exif with non-fatal errors from images.
- jsonrpc: Use Core's logic for computing VcardContact.color ([#7294](https://github.com/chatmail/core/pull/7294)).
### Miscellaneous Tasks
- deps: Bump cachix/install-nix-action from 31.7.0 to 31.8.0.
- cargo: Bump async_zip from 0.0.17 to 0.0.18 ([#7257](https://github.com/chatmail/core/pull/7257)).
- deps: Bump github/codeql-action from 3 to 4 ([#7304](https://github.com/chatmail/core/pull/7304)).
### Refactor
- Use rustls reexported from tokio_rustls.
- Pass ALPN around as &str.
- mimeparser: Store only one signature fingerprint.
### Tests
- Test expiration of ephemeral messages with unknown viewtype.
- Test expiration of non-ephemeral message with unknown viewtype.
## [2.20.0] - 2025-10-13
This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop
when trying to delete a message with unknown viewtype.
### Fixes
- Accept unknown viewtype in ephemeral loop.
- Accept unknown viewtype in delete-old-messages loop.
## [2.19.0] - 2025-10-12
### Features / Changes
- Slightly increase saturation of colors.
### Fixes
- Do not fail to receive call accepted/ended messages referring to non-call Message-ID.
- Do not fail to fully download previously trashed messages.
- Emit AccountsItemChanged when own key is generated/imported, use gray self-color until that ([#7296](https://github.com/chatmail/core/pull/7296)).
- Do not try to process calls from partial messages.
### CI
- Update to Python 3.14.
### Refactor
- Use variables directly in formatted strings ([#7284](https://github.com/chatmail/core/pull/7284)).
- Set_chat_profile_image(): Remove !chat.is_mailing_list() check.
### Miscellaneous Tasks
- cargo: Bump quick-xml from 0.37.5 to 0.38.3.
- Add nodejs to nix dev env ([#7283](https://github.com/chatmail/core/pull/7283))
## [2.18.0] - 2025-10-08
### API-Changes
- [**breaking**] Remove APIs for video chat invitations.
### CI
- nix: Run the workflow when workflow file changes.
- nix: Switch from DeterminateSystems/nix-installer-action to cachix/install-nix-action.
### Features / Changes
- No implicit member changes from old Delta Chat clients ([#7220](https://github.com/chatmail/core/pull/7220)).
### Fixes
- Do not fail to load messages with unknown viewtype.
- Only omit group changes messages if SELF is really added ([#7220](https://github.com/chatmail/core/pull/7220)).
### Refactor
- Assert that Iroh node addresses have home relay URL.
## [2.17.0] - 2025-10-04
### API-Changes
- [**breaking**] Remove deprecated verified_one_on_one_chats config.
### CI
- Require that Cargo.lock is up to date.
- Fix CI checking Nix formatting.
### Documentation
- Comment about outdated timespan.
- Clarify CALL events ([#7188](https://github.com/chatmail/core/pull/7188)).
- Add docs for JS `BaseDeltaChat`.
### Features / Changes
- Make `text/calendar` alternative available as an attachment.
- Better summary for calls.
- Add strings 'You left the channel.' and 'Scan to join Channel' ([#7266](https://github.com/chatmail/core/pull/7266)).
- Stock strings for calls.
- ffi: Add DC_STR_CANT_DECRYPT_OUTGOING_MSGS define.
### Fixes
- Prefer last part in `multipart/alternative`.
- Prefetch messages in limited batches ([#6915](https://github.com/chatmail/core/pull/6915)).
- Forward calls as text messages.
- Consistent spelling of "canceled" with a single "l".
- Lowercase "call" in "Missed call" and similar strings.
### Refactor
- Return the reason when failing to place calls.
### Tests
- Test reception of `multipart/alternative` with `text/calendar`.
## [2.16.0] - 2025-10-01
### API-Changes
- [**breaking**] Get rid of inviter progress other than 0 and 1000.
- Add has_video attribute to incoming call events.
- Add JSON-RPC API to get ICE servers.
- Add call_info() JSON-RPC API.
- Add chat ID to SecureJoinInviterProgress.
- deltachat-rpc-client: Add Chat.resend_messages().
- Add `chat_id` to all call events ([#7216](https://github.com/chatmail/core/pull/7216)).
### Build system
- Update rPGP from 0.16.0 to 0.17.0.
### CI
- Update Rust to 1.90.0.
- Install rustfmt before checking provider database.
### Documentation
- Add more `get_next_event` docs.
- SecurejoinInviterProgress never returns an error.
### Features / Changes
- Don't fetch messages from unknown folders ([#7190](https://github.com/chatmail/core/pull/7190)).
- Get ICE servers from IMAP METADATA.
- Don't ignore receive_imf_inner() errors, try adding partially downloaded message instead ([#7196](https://github.com/chatmail/core/pull/7196)).
- Set dimensions for outgoing Sticker messages.
### Fixes
- Create 1:1 chat only if auth token is for setup contact.
- Ignore vc-/vg- prefix for SecurejoinInviterProgress.
- Don't init Iroh on channel leave ([#7210](https://github.com/chatmail/core/pull/7210)).
- Take the last valid Autocrypt header ([#7167](https://github.com/chatmail/core/pull/7167)).
- Don't add "member removed" messages from nonmembers ([#7207](https://github.com/chatmail/core/pull/7207)).
- Do not consider the call stale if it is not sent out yet.
- Receive_imf: Report replaced message id in `MsgsChanged` if chat is the same.
- Allow Exif for stickers, don't recode them because of that ([#6447](https://github.com/chatmail/core/pull/6447)).
### Refactor
- Remove unused prop (TS, `BaseDeltaChat`).
- Remove unused FolderMeaning::Drafts.
### Tests
- Rename test_udpate_call_text into test_update_call_text.
- Update timestamp_sent in pop_sent_msg_opt().
- Do not match call ID from second alice with first alice event.
## [2.15.0] - 2025-09-15
### API-Changes
- Add JSON-RPC API for calls ([#7194](https://github.com/chatmail/core/pull/7194)).
### Build system
- Remove unused `quoted_printable` dependency.
## [2.14.0] - 2025-09-12
### API-Changes
- Put the chattype into the SecurejoinInviterProgress event ([#7181](https://github.com/chatmail/core/pull/7181)).
### Fixes
- param: Split params only on \n.
- B-encode SDP offer and answer sent in headers.
### Refactor
- Use recv_msg_trash() instead of recv_msg_opt().
- Prepare_msg_raw(): don't return MsgId.
### Tests
- Message is OutFailed if all keys are missing ([#6849](https://github.com/chatmail/core/pull/6849)).
- Test sending SDP offer and answer with newlines.
## [2.13.0] - 2025-09-09
### API-Changes
- [**breaking**] Remove `is_profile_verified` APIs.
- [**breaking**] Remove deprecated `is_protection_broken`.
- [**breaking**] Remove `e2ee_enabled` preference.
### Features / Changes
- Add call ringing API ([#6650](https://github.com/chatmail/core/pull/6650), [#7174](https://github.com/chatmail/core/pull/7174), [#7175](https://github.com/chatmail/core/pull/7175), [#7179](https://github.com/chatmail/core/pull/7179))
- Warn for outdated versions after 6 months instead of 1 year ([#7144](https://github.com/chatmail/core/pull/7144)).
- Do not set "unknown sender for this chat" error.
- Do not replace messages with an error on verification failure.
- Support receiving Autocrypt-Gossip with `_verified` attribute.
- Withdraw all QR codes when one is withdrawn.
### Fixes
- Don't reverify contacts by SELF on receipt of a message from another device.
- Don't verify contacts by others having an unknown verifier.
- Update verifier_id if it's "unknown" and new verifier has known verifier.
- Mark message as failed if it can't be sent ([#7143](https://github.com/chatmail/core/pull/7143)).
- Add "Messages are end-to-end encrypted." to non-protected groups.
### Documentation
- Fix for SecurejoinInviterProgress with progress == 600.
- STYLE.md: Prefer BTreeMap and BTreeSet over hash variants.
### Miscellaneous Tasks
- Update provider database.
- Update dependencies.
### Refactor
- Check that verifier is verified in turn.
- Remove unused `EncryptPreference::Reset`.
- Remove `Aheader::new`.
### Tests
- Add another TimeShiftFalsePositiveNote ([#7142](https://github.com/chatmail/core/pull/7142)).
- Add TestContext.create_chat_id.
## [2.12.0] - 2025-08-26
### API-Changes
- api!(python): remove remaining broken API for reactions
### Features / Changes
- Use Group ID for chat color generation instead of the name for encrypted groups.
- Use key fingerprints instead of addresses for key-contacts color generation.
- Replace HSLuv colors with OKLCh.
- `wal_checkpoint()`: Do `wal_checkpoint(PASSIVE)` and `wal_checkpoint(FULL)` before `wal_checkpoint(TRUNCATE)`.
- Assign messages to key-contacts based on Issuer Fingerprint.
- Create_group_ex(): Log and replace invalid chat name with "…".
### Fixes
- Do not create a group if the sender includes self in the `To` field.
- Do not reverify already verified contacts via gossip.
- `get_connectivity()`: Get rid of locking SchedulerState::inner ([#7124](https://github.com/chatmail/core/pull/7124)).
- Make reaction message hidden only if there are no other parts.
### Refactor
- Do not return `Result` from `valid_signature_fingerprints()`.
- Make `ConnectivityStore` use a non-async lock ([#7129](https://github.com/chatmail/core/pull/7129)).
### Documentation
- Remove broken link from documentation comments.
- Remove the comment about Color Vision Deficiency correction.
## [2.11.0] - 2025-08-13
### Features / Changes
- Contact::lookup_id_by_addr_ex: Prefer returning key-contact.
- Contact::lookup_id_by_addr_ex: Prefer returning accepted contacts.
- Better string when using disappearing messages of one year (365..367 days, so it can be tweaked later).
- Do not require resent messages to be from the same chat.
- `lookup_key_contact_by_address()`: Allow looking up ContactId::SELF without chat id.
- `get_securejoin_qr()`: Log error if group doesn't have grpid.
- `receive_imf::add_parts()`: Get rid of extra `Chat::load_from_db()` calls.
### Fixes
- Ignore case when trying to detect 'invalid unencrypted mail' and add an info-message.
- Run wal_checkpoint during housekeeping ([#6089](https://github.com/chatmail/core/pull/6089)).
- Allow receiving empty files.
- Set correct sent_timestamp for saved outgoing messages.
- Do not remove query parameters from URLs.
- Log and set imex progress error ([#7091](https://github.com/chatmail/core/pull/7091)).
- Do not add key-contacts to unencrypted groups.
- Do not reset `GuaranteeE2ee` in the database when resending messages.
- Assign messages to a group if there is a `Chat-Group-Name`.
- Take `Chat-Group-Name` into account when matching ad hoc groups.
- Don't break long group names with non-ASCII characters.
- Add messages that can't be verified as `DownloadState::Available` ([#7059](https://github.com/chatmail/core/pull/7059)).
### Tests
- Log the number of the test account if there are multiple alices ([#7087](https://github.com/chatmail/core/pull/7087)).
### CI
- Update Rust to 1.89.0.
### Refactor
- Rename icon-address-contact to icon-unencrypted.
- Skip loading the contact of 1:1 unencrypted chat to show the avatar.
- Chat::is_encrypted(): Make one query instead of two for 1:1 chats.
### Miscellaneous Tasks
- cargo: Bump toml from 0.8.23 to 0.9.4.
- cargo: Bump human-panic from 2.0.2 to 2.0.3.
- deny.toml: Add exception for duplicate toml_datetime 0.6.11 dependency.
- deps: Bump actions/checkout from 4 to 5.
- deps: Bump actions/download-artifact from 4 to 5.
## [2.10.0] - 2025-08-04
### Features / Changes
- Also lookup key contacts in lookup_id_by_addr() ([#7073](https://github.com/chatmail/core/pull/7073)).
### Miscellaneous Tasks
- cargo: Bump serde_json from 1.0.140 to 1.0.142.
- cargo: Bump bolero from 0.13.3 to 0.13.4.
- cargo: Bump async-channel from 2.3.1 to 2.5.0.
- cargo: Bump hyper-util from 0.1.14 to 0.1.16.
- cargo: Bump criterion from 0.6.0 to 0.7.0.
- cargo: Bump strum from 0.27.1 to 0.27.2.
- cargo: Bump strum_macros from 0.27.1 to 0.27.2.
- Upgrade async-imap to 0.11.1.
## [2.9.0] - 2025-07-31
### Features / Changes
- repl: Add import-vcard and make-vcard commands ([#7048](https://github.com/chatmail/core/pull/7048)).
### Fixes
- Display correct timer value for ephemeral timer changes.
- Get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker.
### Refactor
- Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts().
- Don't call add_or_lookup_key_contacts() in advance.
## [2.8.0] - 2025-07-28
### Features / Changes
- Remove ProtectionBroken, make such chats Unprotected ([#7041](https://github.com/chatmail/core/pull/7041)).
### Fixes
- Lookup self by address if there is no fingerprint or gossip.
## [2.7.0] - 2025-07-26
### Features / Changes
- Mimefactory: Order message recipients by time of addition ([#6872](https://github.com/chatmail/core/pull/6872)).
- Put the debug/release build version into the info ([#7034](https://github.com/chatmail/core/pull/7034)).
### Fixes
- Realtime late join ([#6869](https://github.com/chatmail/core/pull/6869)).
- Do not fail to upgrade if the verifier of a contact doesn't exist anymore ([#7044](https://github.com/chatmail/core/pull/7044)).
### Tests
- Add regression test for verification-gossiping crash ([#7033](https://github.com/chatmail/core/pull/7033)).
## [2.6.0] - 2025-07-23
### Fixes
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
## [2.5.0] - 2025-07-22
### Fixes
- Correctly migrate "verified by me".
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
- Do not ignore errors in add_flag_finalized_with_set.
### Documentation
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
- STYLE.md: Prefer `try_next()` over `next()`.
## [2.4.0] - 2025-07-21
### Fixes
@@ -1472,7 +2082,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
### Fixes
- Reset quota on configured address change ([#5908](https://github.com/chatmail/core/pull/5908)).
- Do not emit progress 1000 when configuration is cancelled.
- Do not emit progress 1000 when configuration is canceled.
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/chatmail/core/pull/5338)).
- Re-add tokens.foreign_id column ([#6038](https://github.com/chatmail/core/pull/6038)).
@@ -3920,7 +4530,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/chatmail/core/pull/4390)).
- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/chatmail/core/pull/4377)).
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/chatmail/core/pull/4391)).
- Delete `smtp` rows when message sending is canceled ([#4391](https://github.com/chatmail/core/pull/4391)).
### Refactor
@@ -3931,7 +4541,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
### Fixes
- Fetch at most 100 existing messages even if EXISTS was not received.
- Delete `smtp` rows when message sending is cancelled.
- Delete `smtp` rows when message sending is canceled.
### Changes
@@ -4018,14 +4628,14 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
## [1.112.3] - 2023-03-30
### Fixes
- `transfer::get_backup` now frees ongoing process when cancelled. #4249
- `transfer::get_backup` now frees ongoing process when canceled. #4249
## [1.112.2] - 2023-03-30
### Changes
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
- Make sure BackupProvider is canceled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
### Fixes
- Do not return media from trashed messages in the "All media" view. #4247
@@ -6511,3 +7121,25 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
[2.7.0]: https://github.com/chatmail/core/compare/v2.6.0..v2.7.0
[2.8.0]: https://github.com/chatmail/core/compare/v2.7.0..v2.8.0
[2.9.0]: https://github.com/chatmail/core/compare/v2.8.0..v2.9.0
[2.10.0]: https://github.com/chatmail/core/compare/v2.9.0..v2.10.0
[2.11.0]: https://github.com/chatmail/core/compare/v2.10.0..v2.11.0
[2.12.0]: https://github.com/chatmail/core/compare/v2.11.0..v2.12.0
[2.13.0]: https://github.com/chatmail/core/compare/v2.12.0..v2.13.0
[2.14.0]: https://github.com/chatmail/core/compare/v2.13.0..v2.14.0
[2.15.0]: https://github.com/chatmail/core/compare/v2.14.0..v2.15.0
[2.16.0]: https://github.com/chatmail/core/compare/v2.15.0..v2.16.0
[2.17.0]: https://github.com/chatmail/core/compare/v2.16.0..v2.17.0
[2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0
[2.21.0]: https://github.com/chatmail/core/compare/v2.20.0..v2.21.0
[2.22.0]: https://github.com/chatmail/core/compare/v2.21.0..v2.22.0
[2.23.0]: https://github.com/chatmail/core/compare/v2.22.0..v2.23.0
[2.24.0]: https://github.com/chatmail/core/compare/v2.23.0..v2.24.0
[2.25.0]: https://github.com/chatmail/core/compare/v2.24.0..v2.25.0
[2.26.0]: https://github.com/chatmail/core/compare/v2.25.0..v2.26.0

View File

@@ -44,7 +44,7 @@ If you want to contribute a code, follow this guide.
The following prefix types are used:
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is canceled"
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"

635
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.4.0"
version = "2.26.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
@@ -44,14 +44,16 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.11.0", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
blake3 = "1.8.2"
brotli = { version = "8", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
colorutils-rs = { version = "0.7.5", default-features = false }
data-encoding = "2.9.0"
escaper = "0.1"
fast-socks5 = "0.10"
@@ -59,17 +61,16 @@ fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hickory-resolver = "0.25.2"
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"
hyper-util = "0.1.14"
hyper-util = "0.1.16"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.4.3", default-features = false }
mail-builder = { version = "0.4.4", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.17"
@@ -77,18 +78,17 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.16.0", default-features = false }
pgp = { version = "0.17.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.37"
quoted_printable = "0.5"
quick-xml = { version = "0.38", features = ["escape-html"] }
rand-old = { package = "rand", version = "0.8" }
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.12.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
sdp = "0.8.0"
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
@@ -104,19 +104,18 @@ thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
astral-tokio-tar = { version = "0.5.6", default-features = false }
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.8"
toml = "0.9"
tracing = "0.1.41"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.8"
blake3 = "1.8.2"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.6.0", features = ["async_tokio"] }
criterion = { version = "0.7.0", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -157,6 +156,11 @@ name = "receive_emails"
required-features = ["internals"]
harness = false
[[bench]]
name = "decrypting"
required-features = ["internals"]
harness = false
[[bench]]
name = "get_chat_msgs"
harness = false
@@ -175,29 +179,29 @@ harness = false
[workspace.dependencies]
anyhow = "1"
async-channel = "2.3.1"
async-channel = "2.5.0"
base64 = "0.22"
chrono = { version = "0.4.41", default-features = false }
chrono = { version = "0.4.42", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
futures = "0.3.31"
futures-lite = "2.6.0"
futures-lite = "2.6.1"
libc = "0.2"
log = "0.4"
mailparse = "0.16.1"
nu-ansi-term = "0.46"
nu-ansi-term = "0.50"
num-traits = "0.2"
rand = "0.8"
rand = "0.9"
regex = "1.10"
rusqlite = "0.36"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.20.0"
tempfile = "3.23.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.14"
tokio-util = "0.7.16"
tracing-subscriber = "0.3"
yerpc = "0.6.4"

View File

@@ -80,18 +80,26 @@ Connect to your mail server (if already configured):
> connect
```
Create a contact:
Export your public key to a vCard file:
```
> make-vcard my.vcard 1
```
Create contacts by address or vCard file:
```
> addcontact yourfriends@email.org
> import-vcard key-contact.vcard
```
List contacts:
```
> listcontacts
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
Contact#Contact#Self: Me √ <your@email.org>
1 key contacts.
2 key contacts.
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
1 address contacts.
```
@@ -189,12 +197,10 @@ and then run the script.
Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- -> libdeltachat is going to be deprecated and only exists because Android, iOS and Ubuntu Touch are still using it. If you build a new project, then please use the jsonrpc api instead.
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go**
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
- over cffi[^1]: \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Go** \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library
@@ -207,5 +213,3 @@ or its language bindings:
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
- several **Bots**
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.

View File

@@ -78,6 +78,27 @@ All errors should be handled in one of these ways:
- With `.log_err().ok()`.
- Bubbled up with `?`.
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
```
while let Some(event) = stream.try_next().await? {
todo!();
}
```
instead of
```
while let Some(event_res) = stream.next().await {
todo!();
}
```
as it allows bubbling up the error early with `?`
with no way to accidentally skip error processing
with early `continue` or `break`.
Some streams reading from a connection
return infinite number of `Some(Err(_))`
items when connection breaks and not processing
errors may result in infinite loop.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`
@@ -91,6 +112,18 @@ Follow
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
for `.expect` message style.
## BTreeMap vs HashMap
Prefer [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html)
over [HashMap](https://doc.rust-lang.org/std/collections/struct.HashMap.html)
and [BTreeSet](https://doc.rust-lang.org/std/collections/struct.BTreeSet.html)
over [HashSet](https://doc.rust-lang.org/std/collections/struct.HashSet.html)
as iterating over these structures returns items in deterministic order.
Non-deterministic code may result in difficult to reproduce bugs,
flaky tests, regression tests that miss bugs
or different behavior on different devices when processing the same messages.
## Logging
For logging, use `info!`, `warn!` and `error!` macros.

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

200
benches/decrypting.rs Normal file
View File

@@ -0,0 +1,200 @@
//! Benchmarks for message decryption,
//! comparing decryption of symmetrically-encrypted messages
//! to decryption of asymmetrically-encrypted messages.
//!
//! Call with
//!
//! ```text
//! cargo bench --bench decrypting --features="internals"
//! ```
//!
//! or, if you want to only run e.g. the 'Decrypt a symmetrically encrypted message' benchmark:
//!
//! ```text
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt a symmetrically encrypted message'
//! ```
//!
//! You can also pass a substring.
//! So, you can run all 'Decrypt and parse' benchmarks with:
//!
//! ```text
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse'
//! ```
//!
//! Symmetric decryption has to try out all known secrets,
//! You can benchmark this by adapting the `NUM_SECRETS` variable.
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::internals_for_benches::create_broadcast_secret;
use deltachat::internals_for_benches::create_dummy_keypair;
use deltachat::internals_for_benches::save_broadcast_secret;
use deltachat::{
Events,
chat::ChatId,
config::Config,
context::Context,
internals_for_benches::key_from_asc,
internals_for_benches::parse_and_get_text,
internals_for_benches::store_self_keypair,
pgp::{KeyPair, decrypt, pk_encrypt, symm_encrypt_message},
stock_str::StockStrings,
};
use rand::{Rng, rng};
use tempfile::tempdir;
const NUM_SECRETS: usize = 500;
async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let context = Context::new(dbfile.as_path(), 100, Events::new(), StockStrings::new())
.await
.unwrap();
context
.set_config(Config::ConfiguredAddr, Some("bob@example.net"))
.await
.unwrap();
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
let public = secret.signed_public_key();
let key_pair = KeyPair { public, secret };
store_self_keypair(&context, &key_pair)
.await
.expect("Failed to save key");
context
}
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("Decrypt");
// ===========================================================================================
// Benchmarks for decryption only, without any other parsing
// ===========================================================================================
group.sample_size(10);
group.bench_function("Decrypt a symmetrically encrypted message", |b| {
let plain = generate_plaintext();
let secrets = generate_secrets();
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
let secret = secrets[NUM_SECRETS / 2].clone();
symm_encrypt_message(
plain.clone(),
create_dummy_keypair("alice@example.org").unwrap().secret,
black_box(&secret),
true,
)
.await
.unwrap()
});
b.iter(|| {
let mut msg =
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
let decrypted = msg.as_data_vec().unwrap();
assert_eq!(black_box(decrypted), plain);
});
});
group.bench_function("Decrypt a public-key encrypted message", |b| {
let plain = generate_plaintext();
let key_pair = create_dummy_keypair("alice@example.org").unwrap();
let secrets = generate_secrets();
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
pk_encrypt(
plain.clone(),
vec![black_box(key_pair.public.clone())],
Some(key_pair.secret.clone()),
true,
true,
)
.await
.unwrap()
});
b.iter(|| {
let mut msg = decrypt(
encrypted.clone().into_bytes(),
std::slice::from_ref(&key_pair.secret),
black_box(&secrets),
)
.unwrap();
let decrypted = msg.as_data_vec().unwrap();
assert_eq!(black_box(decrypted), plain);
});
});
// ===========================================================================================
// Benchmarks for the whole parsing pipeline, incl. decryption (but excl. receive_imf())
// ===========================================================================================
let rt = tokio::runtime::Runtime::new().unwrap();
let mut secrets = generate_secrets();
// "secret" is the shared secret that was used to encrypt text_symmetrically_encrypted.eml.
// Put it into the middle of our secrets:
secrets[NUM_SECRETS / 2] = "secret".to_string();
let context = rt.block_on(async {
let context = create_context().await;
for (i, secret) in secrets.iter().enumerate() {
save_broadcast_secret(&context, ChatId::new(10 + i as u32), secret)
.await
.unwrap();
}
context
});
group.bench_function("Decrypt and parse a symmetrically encrypted message", |b| {
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
let text = parse_and_get_text(
&ctx,
include_bytes!("../test-data/message/text_symmetrically_encrypted.eml"),
)
.await
.unwrap();
assert_eq!(text, "Symmetrically encrypted message");
}
});
});
group.bench_function("Decrypt and parse a public-key encrypted message", |b| {
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
let text = parse_and_get_text(
&ctx,
include_bytes!("../test-data/message/text_from_alice_encrypted.eml"),
)
.await
.unwrap();
assert_eq!(text, "hi");
}
});
});
group.finish();
}
fn generate_secrets() -> Vec<String> {
let secrets: Vec<String> = (0..NUM_SECRETS)
.map(|_| create_broadcast_secret())
.collect();
secrets
}
fn generate_plaintext() -> Vec<u8> {
let mut plain: Vec<u8> = vec![0; 500];
rng().fill(&mut plain[..]);
plain
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -68,7 +68,7 @@ impl ContactAddress {
pub fn new(s: &str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(&addr) {
bail!("invalid address {:?}", s);
bail!("invalid address {s:?}");
}
Ok(Self(addr.to_string()))
}
@@ -257,16 +257,16 @@ impl EmailAddress {
.chars()
.any(|c| c.is_whitespace() || c == '<' || c == '>')
{
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
bail!("Email {input:?} must not contain whitespaces, '>' or '<'");
}
match &parts[..] {
[domain, local] => {
if local.is_empty() {
bail!("empty string is not valid for local part in {:?}", input);
bail!("empty string is not valid for local part in {input:?}");
}
if domain.is_empty() {
bail!("missing domain after '@' in {:?}", input);
bail!("missing domain after '@' in {input:?}");
}
if domain.ends_with('.') {
bail!("Domain {domain:?} should not contain the dot in the end");
@@ -276,7 +276,7 @@ impl EmailAddress {
domain: (*domain).to_string(),
})
}
_ => bail!("Email {:?} must contain '@' character", input),
_ => bail!("Email {input:?} must contain '@' character"),
}
}
}

View File

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

View File

@@ -415,7 +415,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* As for `displayname` and `selfstatus`, also the avatar is sent to the recipients.
* To save traffic, however, the avatar is attached only as needed
* and also recoded to a reasonable size.
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts
* default=send and request read receipts, only send but not request if `bot` is set
@@ -459,12 +458,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* The library uses the `media_quality` setting to use different defaults
* for recoding images sent with type #DC_MSG_IMAGE.
* If needed, recoding other file types is up to the UI.
* - `webrtc_instance` = webrtc instance to use for videochats in the form
* `[basicwebrtc:|jitsi:]https://example.com/subdir#roomname=$ROOM`
* if the URL is prefixed by `basicwebrtc`, the server is assumed to be of the type
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
* The type `jitsi:` may be handled by external apps.
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages,
@@ -503,13 +496,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
* to 1 if it supports verified 1:1 chats.
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
* and when the key changes, an info message is posted into the chat.
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `is_muted` = Whether a context is muted by the user.
* Muted contexts should not sound, vibrate or show notifications.
@@ -583,11 +569,10 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
/**
* Set configuration values from a QR code.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
* QR code is DC_QR_ACCOUNT or DC_QR_LOGIN.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN.
*
* @memberof dc_context_t
* @param context The context object.
@@ -1060,42 +1045,6 @@ void dc_send_edit_request (dc_context_t* context, uint32_t ms
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
/**
* Send invitation to a videochat.
*
* This function reads the `webrtc_instance` config value,
* may check that the server is working in some way
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
*
* After that, the function sends out a message that contains information to join the room:
*
* - To allow non-delta-clients to join the chat,
* the message contains a text-area with some descriptive text
* and a URL that can be opened in a supported browser to join the videochat.
*
* - delta-clients can get all information needed from
* the message object, using e.g.
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION.
*
* dc_send_videochat_invitation() is blocking and may take a while,
* so the UIs will typically call the function from within a thread.
* Moreover, UIs will typically enter the room directly without an additional click on the message,
* for this purpose, the function returns the message id directly.
*
* As for other messages sent, this function
* sends the event #DC_EVENT_MSGS_CHANGED on success, the message has a delivery state, and so on.
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
* However, UIs might some things differently, e.g. play a different sound.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat to start a videochat for.
* @return The ID of the message sent out
* or 0 for errors.
*/
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* A webxdc instance sends a status update to its other members.
*
@@ -1222,6 +1171,117 @@ void dc_set_webxdc_integration (dc_context_t* context, const char* f
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
/**
* Start an outgoing call.
* This sends a message of type #DC_MSG_CALL with all relevant information to the callee,
* who will get informed by an #DC_EVENT_INCOMING_CALL event and rings.
*
* Possible actions during ringing:
*
* - caller cancels the call using dc_end_call():
* callee receives #DC_EVENT_CALL_ENDED and has a "Missed call"
*
* - callee accepts using dc_accept_incoming_call():
* caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED.
* callee's devices receive #DC_EVENT_INCOMING_CALL_ACCEPTED, call starts
*
* - callee declines using dc_end_call():
* caller receives #DC_EVENT_CALL_ENDED and has a "Declinced Call".
* callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Canceled Call",
*
* - callee is already in a call:
* what to do depends on the capabilities of UI to handle calls.
* if UI cannot handle multiple calls, an easy approach would be to decline the new call automatically
* and make that visble to the user in the call, e.g. by a notification
*
* - timeout:
* after 1 minute without action,
* caller and callee receive #DC_EVENT_CALL_ENDED
* to prevent endless ringing of callee
* in case caller got offline without being able to send cancellation message.
* for caller, this is a "Canceled call";
* for callee, this is a "Missed call"
*
* Actions during the call:
*
* - caller ends the call using dc_end_call():
* callee receives #DC_EVENT_CALL_ENDED
*
* - callee ends the call using dc_end_call():
* caller receives #DC_EVENT_CALL_ENDED
*
* Contact request handling:
*
* - placing or accepting calls implies accepting contact requests
*
* - ending a call does not accept a contact request;
* instead, the call will timeout on all affected devices.
*
* Note, that the events are for updating the call screen,
* possible status messages are added and updated as usual, including the known events.
* In the UI, the sorted chatlist is used as an overview about calls as well as messages.
* To place a call with a contact that has no chat yet, use dc_create_chat_by_contact_id() first.
*
* UI will usually allow only one call at the same time,
* this has to be tracked by UI across profile, the core does not track this.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat to place a call for.
* This needs to be a one-to-one chat.
* @param place_call_info any data that other devices receive
* in #DC_EVENT_INCOMING_CALL.
* @return ID of the system message announcing the call.
*/
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info);
/**
* Accept incoming call.
*
* This implicitly accepts the contact request, if not yet done.
* All affected devices will receive
* either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED.
*
* If the call is already accepted or ended, nothing happens.
* If the chat is a contact request, it is accepted implicitly.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the call to accept.
* This is the ID reported by #DC_EVENT_INCOMING_CALL
* and equals to the ID of the corresponding info message.
* @param accept_call_info any data that other devices receive
* in #DC_EVENT_OUTGOING_CALL_ACCEPTED.
* @return 1=success, 0=error
*/
int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id, const char* accept_call_info);
/**
* End incoming or outgoing call.
*
* For unaccepted calls ended by the caller, this is a "cancellation".
* Unaccepted calls ended by the callee are a "decline".
* If the call was accepted, this is a "hangup".
*
* All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED unless they are contact requests.
* For contact requests, the call times out on all other affected devices.
*
* If the message ID is wrong or does not exist for whatever reasons, nothing happens.
* Therefore, and for resilience, UI should remove the call UI directly when calling
* this function and not only on the event.
*
* If the call is already ended, nothing happens.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id the ID of the call.
* @return 1=success, 0=error
*/
int dc_end_call (dc_context_t* context, uint32_t msg_id);
/**
* Save a draft for a chat in the database.
*
@@ -1339,12 +1399,14 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
* Optionally, some special markers added to the ID array may help to
* implement virtual lists.
*
* To get the concrete time of the message, use dc_array_get_timestamp().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID of which the messages IDs should be queried.
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
* To get the concrete time of the marker, use dc_array_get_timestamp().
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
* @param marker1before Deprecated, set this to 0.
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
@@ -1701,9 +1763,7 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
*
* @memberof dc_context_t
* @param context The context object.
* @param protect If set to 1 the function creates group with protection initially enabled.
* Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled.
* @param protect Deprecated 2025-08-31, ignored.
* @param name The name of the group chat to create.
* The name may be changed later using dc_set_chat_name().
* To find out the name of a group later, see dc_chat_get_name()
@@ -2094,9 +2154,19 @@ int dc_may_be_valid_addr (const char* addr);
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* Looks up a known and unblocked contact with a given e-mail address.
* To get a list of all known and unblocked contacts, use dc_get_contacts().
*
* **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
* (e.g. an address-contact and a key-contact),
* this looks up the most recently seen contact,
* i.e. which contact is returned depends on which contact last sent a message.
* If the user just clicked on a mailto: link, then this is the best thing you can do.
* But **DO NOT** internally represent contacts by their email address
* and do not use this function to look them up;
* otherwise this function will sometimes look up the wrong contact.
* Instead, you should internally represent contacts by their ids.
*
* To validate an e-mail address independently of the contact database
* use dc_may_be_valid_addr().
*
@@ -2118,6 +2188,13 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
* a bunch of addresses.
*
* This will always create or look up an address-contact,
* i.e. a contact identified by an email address,
* with all messages sent to and from this contact being unencrypted.
* If the user just clicked on an email address,
* you should first check `lookup_contact_id_by_addr`,
* and only if there is no contact yet, call this function here.
*
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
*
* @memberof dc_context_t
@@ -2486,6 +2563,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_ASK_VERIFYCONTACT 200 // id=contact
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
#define DC_QR_ASK_VERIFYBROADCAST 204 // text1=broadcast name
#define DC_QR_FPR_OK 210 // id=contact
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
@@ -2493,7 +2571,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_BACKUP 251 // deprecated
#define DC_QR_BACKUP2 252
#define DC_QR_BACKUP_TOO_NEW 255
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
@@ -2519,8 +2596,9 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask whether to verify the contact;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_ASK_VERIFYGROUP with dc_lot_t::text1=Group name:
* ask whether to join the group;
* - DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
* with dc_lot_t::text1=Group name:
* ask whether to join the chat;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
@@ -2547,10 +2625,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* show a hint to the user that this backup comes from a newer Delta Chat version
* and this device needs an update
*
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_PROXY with dc_lot_t::text1=address:
* ask the user if they want to use the given proxy.
* if so, call dc_set_config_from_qr() and restart I/O.
@@ -2607,7 +2681,8 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* if dc_check_qr() returns
* DC_QR_ASK_VERIFYCONTACT, DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* The returned text will also work as a normal https:-link,
@@ -2648,7 +2723,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with dc_get_securejoin_qr().
* This function is typically called when dc_check_qr() returns
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
* lot.state=DC_QR_ASK_VERIFYCONTACT, lot.state=DC_QR_ASK_VERIFYGROUP or lot.state=DC_QR_ASK_VERIFYBROADCAST
*
* The function returns immediately and the handshake runs in background,
* sending and receiving several messages.
@@ -3816,27 +3891,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
/**
* Check if a chat is protected.
*
* End-to-end encryption is guaranteed in protected chats
* and only verified contacts
* as determined by dc_contact_is_verified()
* can be added to protected chats.
*
* Protected chats are created using dc_create_group_chat()
* by setting the 'protect' parameter to 1.
* 1:1 chats become protected or unprotected automatically
* if `verified_one_on_one_chats` setting is enabled.
*
* UI should display a green checkmark
* in the chat title,
* in the chatlist item
* and in the chat profile
* if chat protection is enabled.
* Deprecated, always returns 0.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protected, 0=chat is not protected.
* @return Always 0.
* @deprecated 2025-09-09
*/
int dc_chat_is_protected (const dc_chat_t* chat);
@@ -3856,26 +3916,6 @@ int dc_chat_is_protected (const dc_chat_t* chat);
int dc_chat_is_encrypted (const dc_chat_t *chat);
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
*
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
* otherwise it will return false for all chats.
*
* 1:1 chats are automatically set as protected when a contact is verified.
* When a message comes in that is not encrypted / signed correctly,
* the chat is automatically set as unprotected again.
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
*
* The UI should let the user confirm that this is OK with a message like
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
*/
int dc_chat_is_protection_broken (const dc_chat_t* chat);
/**
* Check if locations are sent to the chat
* at the time the object was created using dc_get_chat().
@@ -4651,22 +4691,6 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
/**
* Get URL of a videochat invitation.
*
* Videochat invitations are sent out using dc_send_videochat_invitation()
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return If the message contains a videochat invitation,
* the URL of the invitation is returned.
* If the message is no videochat invitation, NULL is returned.
* Must be released using dc_str_unref() when done.
*/
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
/**
* Gets the error status of the message.
* If there is no error associated with the message, NULL is returned.
@@ -4689,41 +4713,6 @@ char* dc_msg_get_videochat_url (const dc_msg_t* msg);
char* dc_msg_get_error (const dc_msg_t* msg);
/**
* Get type of videochat.
*
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
* in this case, if `basicwebrtc:` as of https://github.com/cracker0dks/basicwebrtc or `jitsi`
* were used to initiate the videochat,
* dc_msg_get_videochat_type() returns the corresponding type.
*
* The videochat URL can be retrieved using dc_msg_get_videochat_url().
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC, DC_VIDEOCHATTYPE_JITSI or DC_VIDEOCHATTYPE_UNKNOWN.
*
* Example:
* ~~~
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
* // videochat invitation that we ship a client for
* } else {
* // use browser for videochat - or add an additional check for DC_VIDEOCHATTYPE_JITSI
* }
* } else {
* // not a videochat invitation
* }
* ~~~
*/
int dc_msg_get_videochat_type (const dc_msg_t* msg);
#define DC_VIDEOCHATTYPE_UNKNOWN 0
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
#define DC_VIDEOCHATTYPE_JITSI 2
/**
* Checks if the message has a full HTML version.
*
@@ -5267,20 +5256,14 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if the contact
* can be added to verified chats,
* i.e. has a verified key
* and Autocrypt key matches the verified key.
* can be added to protected chats.
*
* If contact is verified
* UI should display green checkmark after the contact name
* in contact list items,
* in chat member list items
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
* See dc_contact_get_verifier_id() for a guidance how to display these information.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
* 2: SELF and contact have verified their fingerprints in both directions.
*/
int dc_contact_is_verified (dc_contact_t* contact);
@@ -5311,16 +5294,22 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
*
* If the function returns non-zero result,
* display green checkmark in the profile and "Introduced by ..." line
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr.
* As verifier may be unknown,
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
*
* If this function returns a verifier,
* this does not necessarily mean
* you can add the contact to verified chats.
* Use dc_contact_is_verified() to check
* if a contact can be added to a verified chat instead.
* UI should display the information in the contact's profile as follows:
*
* - 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().
* Prefix the text by a green checkmark.
*
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
* display "Introduced" prefixed by a green checkmark.
*
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
* display nothing
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5356,11 +5345,9 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
/**
* Create a provider struct for the given e-mail address by local and DNS lookup.
* Create a provider struct for the given e-mail address by local lookup.
*
* First lookup is done from the local database as of dc_provider_new_from_email().
* If the first lookup fails, an additional DNS lookup is done,
* trying to figure out the provider belonging to custom domains.
* DNS lookup is not used anymore and this function is deprecated.
*
* @memberof dc_provider_t
* @param context The context object.
@@ -5368,6 +5355,7 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
*/
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
@@ -5623,14 +5611,21 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Message indicating an incoming or outgoing videochat.
* The message was created via dc_send_videochat_invitation() on this or a remote device.
* Message indicating an incoming or outgoing call.
*
* Typically, such messages are rendered differently by the UIs,
* e.g. contain a button to join the videochat.
* The URL for joining can be retrieved using dc_msg_get_videochat_url().
* These messages are created by dc_place_outgoing_call()
* and should be rendered by UI similar to text messages,
* maybe with some "phone icon" at the side.
*
* The message text is updated as needed
* and UI will be informed via #DC_EVENT_MSGS_CHANGED as usual.
*
* Do not start ringing when seeing this message;
* the mesage may belong e.g. to an old missed call.
*
* Instead, ringing should start on the event #DC_EVENT_INCOMING_CALL
*/
#define DC_MSG_VIDEOCHAT_INVITATION 70
#define DC_MSG_CALL 71
/**
@@ -6386,7 +6381,6 @@ void dc_event_unref(dc_event_t* event);
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
* and dc_remove_contact_from_chat().
*
@@ -6476,11 +6470,7 @@ void dc_event_unref(dc_event_t* event);
* generated by dc_get_securejoin_qr().
*
* @param data1 (int) The ID of the contact that wants to join.
* @param data2 (int) The progress as:
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
* 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
* 1000=Protocol finished for this contact.
* @param data2 (int) The progress, always 1000.
*/
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
@@ -6632,6 +6622,60 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_CHANNEL_OVERFLOW 2400
/**
* Incoming call.
* UI will usually start ringing,
* or show a notification if there is already a call in some profile.
*
* Together with this event,
* a message of type #DC_MSG_CALL is added to the corresponding chat;
* this message is announced and updated by the usual event as #DC_EVENT_MSGS_CHANGED,
* there is usually no need to take care of this message from any of the CALL events.
*
* If user takes action, dc_accept_incoming_call() or dc_end_call() should be called.
*
* Otherwise, ringing should end on #DC_EVENT_CALL_ENDED
* or #DC_EVENT_INCOMING_CALL_ACCEPTED
*
* @param data1 (int) msg_id ID of the message referring to the call.
* @param data2 (char*) place_call_info, text passed to dc_place_outgoing_call()
* @param data2 (int) 1 if incoming call is a video call, 0 otherwise
*/
#define DC_EVENT_INCOMING_CALL 2550
/**
* The callee accepted an incoming call on this or another device using dc_accept_incoming_call().
* The caller gets the event #DC_EVENT_OUTGOING_CALL_ACCEPTED at the same time.
*
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
* @param data1 (int) msg_id ID of the message referring to the call
*/
#define DC_EVENT_INCOMING_CALL_ACCEPTED 2560
/**
* A call placed using dc_place_outgoing_call() was accepted by the callee using dc_accept_incoming_call().
*
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
* @param data1 (int) msg_id ID of the message referring to the call
* @param data2 (char*) accept_call_info, text passed to dc_accept_incoming_call()
*/
#define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570
/**
* An incoming or outgoing call was ended using dc_end_call() on this or another device, by caller or callee.
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
*
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
* @param data1 (int) msg_id ID of the message referring to the call
*/
#define DC_EVENT_CALL_ENDED 2580
/**
* @}
*/
@@ -6915,11 +6959,6 @@ void dc_event_unref(dc_event_t* event);
/// Used to build the string returned by dc_get_contact_encrinfo().
#define DC_STR_ENCR_NONE 28
/// "This message was encrypted for another setup."
///
/// Used as message text if decryption fails.
#define DC_STR_CANTDECRYPT_MSG_BODY 29
/// "Fingerprints"
///
/// Used to build the string returned by dc_get_contact_encrinfo().
@@ -7052,6 +7091,8 @@ void dc_event_unref(dc_event_t* event);
/// "Unknown sender for this chat. See 'info' for more details."
///
/// Use as message text if assigning the message to a chat is not totally correct.
///
/// @deprecated 2025-08-18
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
/// "Message from %1$s"
@@ -7114,17 +7155,6 @@ void dc_event_unref(dc_event_t* event);
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
/// "Video chat invitation"
///
/// Used in summaries.
#define DC_STR_VIDEOCHAT_INVITATION 82
/// "You are invited to a video chat, click %1$s to join."
///
/// Used as message text of outgoing video chat invitations.
/// - %1$s will be replaced by the URL of the video chat
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
/// "Error: %1$s"
///
/// Used in error strings.
@@ -7423,7 +7453,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
/// "You left."
/// "You left the group."
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_YOU 132
@@ -7594,6 +7624,18 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You set message deletion timer to 1 year."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_YOU 158
/// "Message deletion timer is set to 1 year by %1$s."
///
/// `%1$s` will be replaced by name and address 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.
@@ -7670,8 +7712,47 @@ void dc_event_unref(dc_event_t* event);
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
#define DC_STR_DONATION_REQUEST 193
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200
/// "Outgoing call"
#define DC_STR_OUTGOING_CALL 194
/// "Incoming call"
#define DC_STR_INCOMING_CALL 195
/// "Declined call"
#define DC_STR_DECLINED_CALL 196
/// "Canceled call"
#define DC_STR_CANCELED_CALL 197
/// "Missed call"
#define DC_STR_MISSED_CALL 198
/// "You left the channel."
///
/// Used in status messages.
#define DC_STR_CHANNEL_LEFT_BY_YOU 200
/// "Scan to join channel %1$s"
///
/// Subtitle for channel join qrcode svg image generated by the core.
///
/// `%1$s` will be replaced with the channel name.
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
/// "Proxy Enabled"
///
/// Title for proxy section in connectivity view.
#define DC_STR_PROXY_ENABLED 220
/// "You are using a proxy. If you're having trouble connecting, try a different proxy."
///
/// Description in connectivity view when proxy is enabled.
#define DC_STR_PROXY_ENABLED_DESCRIPTION 221
/// "Messages in this chat use classic email and are not encrypted."
///
/// Used as the first info messages in newly created classic email threads.
#define DC_STR_CHAT_UNENCRYPTED_EXPLANATON 230
/**
* @}

View File

@@ -22,7 +22,7 @@ use std::sync::{Arc, LazyLock};
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::{Context, ContextBuilder};
@@ -39,7 +39,6 @@ use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use message::Viewtype;
use num_traits::{FromPrimitive, ToPrimitive};
use rand::Rng;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
@@ -101,7 +100,7 @@ pub unsafe extern "C" fn dc_context_new(
let ctx = if blobdir.is_null() || *blobdir == 0 {
// generate random ID as this functionality is not yet available on the C-api.
let id = rand::thread_rng().gen();
let id = rand::random();
block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
@@ -129,7 +128,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
return ptr::null_mut();
}
let id = rand::thread_rng().gen();
let id = rand::random();
match block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
@@ -375,7 +374,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
return 0;
}
let ctx = &*context;
block_on(ctx.get_connectivity()) as u32 as libc::c_int
ctx.get_connectivity() as u32 as libc::c_int
}
#[no_mangle]
@@ -556,6 +555,10 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::AccountsChanged => 2302,
EventType::AccountsItemChanged => 2303,
EventType::EventChannelOverflow { .. } => 2400,
EventType::IncomingCall { .. } => 2550,
EventType::IncomingCallAccepted { .. } => 2560,
EventType::OutgoingCallAccepted { .. } => 2570,
EventType::CallEnded { .. } => 2580,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -619,7 +622,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
EventType::WebxdcRealtimeData { msg_id, .. }
| EventType::WebxdcStatusUpdate { msg_id, .. }
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
| EventType::WebxdcInstanceDeleted { msg_id, .. }
| EventType::IncomingCall { msg_id, .. }
| EventType::IncomingCallAccepted { msg_id, .. }
| EventType::OutgoingCallAccepted { msg_id, .. }
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::ChatlistItemChanged { chat_id } => {
chat_id.unwrap_or_default().to_u32() as libc::c_int
}
@@ -671,6 +678,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ChatModified(_)
| EventType::ChatDeleted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::IncomingCallAccepted { .. }
| EventType::OutgoingCallAccepted { .. }
| EventType::CallEnded { .. }
| EventType::EventChannelOverflow { .. } => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
@@ -689,6 +699,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
..
} => status_update_serial.to_u32() as libc::c_int,
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -767,8 +779,21 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
| EventType::IncomingCallAccepted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
EventType::IncomingCall {
place_call_info, ..
} => {
let data2 = place_call_info.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::OutgoingCallAccepted {
accept_call_info, ..
} => {
let data2 = accept_call_info.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
@@ -1072,25 +1097,6 @@ pub unsafe extern "C" fn dc_send_delete_request(
.ok();
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_videochat_invitation(
context: *mut dc_context_t,
chat_id: u32,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
return 0;
}
let ctx = &*context;
block_on(async move {
chat::send_videochat_invitation(ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send video chat invitation")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
@@ -1167,6 +1173,61 @@ pub unsafe extern "C" fn dc_init_webxdc_integration(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_place_outgoing_call(
context: *mut dc_context_t,
chat_id: u32,
place_call_info: *const libc::c_char,
) -> u32 {
if context.is_null() || chat_id == 0 {
eprintln!("ignoring careless call to dc_place_outgoing_call()");
return 0;
}
let ctx = &*context;
let chat_id = ChatId::new(chat_id);
let place_call_info = to_string_lossy(place_call_info);
block_on(ctx.place_outgoing_call(chat_id, place_call_info))
.context("Failed to place call")
.log_err(ctx)
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to place call")
}
#[no_mangle]
pub unsafe extern "C" fn dc_accept_incoming_call(
context: *mut dc_context_t,
msg_id: u32,
accept_call_info: *const libc::c_char,
) -> libc::c_int {
if context.is_null() || msg_id == 0 {
eprintln!("ignoring careless call to dc_accept_incoming_call()");
return 0;
}
let ctx = &*context;
let msg_id = MsgId::new(msg_id);
let accept_call_info = to_string_lossy(accept_call_info);
block_on(ctx.accept_incoming_call(msg_id, accept_call_info))
.context("Failed to accept call")
.is_ok() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_end_call(context: *mut dc_context_t, msg_id: u32) -> libc::c_int {
if context.is_null() || msg_id == 0 {
eprintln!("ignoring careless call to dc_end_call()");
return 0;
}
let ctx = &*context;
let msg_id = MsgId::new(msg_id);
block_on(ctx.end_call(msg_id))
.context("Failed to end call")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -1659,7 +1720,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
#[no_mangle]
pub unsafe extern "C" fn dc_create_group_chat(
context: *mut dc_context_t,
protect: libc::c_int,
_protect: libc::c_int,
name: *const libc::c_char,
) -> u32 {
if context.is_null() || name.is_null() {
@@ -1667,22 +1728,12 @@ pub unsafe extern "C" fn dc_create_group_chat(
return 0;
}
let ctx = &*context;
let Some(protect) = ProtectionStatus::from_i32(protect)
.context("Bad protect-value for dc_create_group_chat()")
.log_err(ctx)
.ok()
else {
return 0;
};
block_on(async move {
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
.await
.context("Failed to create group chat")
.log_err(ctx)
.map(|id| id.to_u32())
.unwrap_or(0)
})
block_on(chat::create_group(ctx, &to_string_lossy(name)))
.context("Failed to create group chat")
.log_err(ctx)
.map(|id| id.to_u32())
.unwrap_or(0)
}
#[no_mangle]
@@ -3144,13 +3195,8 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protected()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protected() as libc::c_int
pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int {
0
}
#[no_mangle]
@@ -3165,16 +3211,6 @@ pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_i
.unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protection_broken() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
@@ -3783,31 +3819,6 @@ pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
ffi_msg.message.has_html().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg
.message
.get_videochat_url()
.unwrap_or_default()
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
@@ -4229,7 +4240,17 @@ pub unsafe extern "C" fn dc_contact_get_color(contact: *mut dc_contact_t) -> u32
return 0;
}
let ffi_contact = &*contact;
ffi_contact.contact.get_color()
let ctx = &*ffi_contact.context;
block_on(async move {
ffi_contact
.contact
// We don't want any UIs displaying gray self-color.
.get_or_gen_color(ctx)
.await
.context("Contact::get_color()")
.log_err(ctx)
.unwrap_or(0)
})
}
#[no_mangle]
@@ -4634,13 +4655,9 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
true,
))
.log_err(ctx)
.unwrap_or_default()
match provider::get_provider_info_by_addr(addr.as_str())
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
@@ -4659,25 +4676,13 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
let addr = to_string_lossy(addr);
let ctx = &*context;
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
.context("Can't get config")
.log_err(ctx);
match proxy_enabled {
Ok(proxy_enabled) => {
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
proxy_enabled,
))
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
}
Err(_) => ptr::null_mut(),
match provider::get_provider_info_by_addr(addr.as_str())
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
}

View File

@@ -45,13 +45,13 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => None,
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
Qr::FprOk { .. } => None,
Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
Qr::Backup2 { .. } => None,
Qr::BackupTooNew { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
Qr::Url { url } => Some(Cow::Borrowed(url)),
@@ -99,13 +99,13 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
Qr::FprOk { .. } => LotState::QrFprOk,
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
Qr::Account { .. } => LotState::QrAccount,
Qr::Backup2 { .. } => LotState::QrBackup2,
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Proxy { .. } => LotState::QrProxy,
Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl,
@@ -126,13 +126,13 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::AskVerifyGroup { .. } => Default::default(),
Qr::AskJoinBroadcast { .. } => Default::default(),
Qr::FprOk { contact_id } => contact_id.to_u32(),
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::Backup2 { .. } => Default::default(),
Qr::BackupTooNew { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Proxy { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
@@ -169,6 +169,9 @@ pub enum LotState {
/// text1=groupname
QrAskVerifyGroup = 202,
/// text1=broadcast_name
QrAskJoinBroadcast = 204,
/// id=contact
QrFprOk = 210,
@@ -185,9 +188,6 @@ pub enum LotState {
QrBackupTooNew = 255,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// text1=address, text2=protocol
QrProxy = 271,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.4.0"
version = "2.26.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -8,10 +8,10 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::blob::BlobObject;
use deltachat::calls::ice_servers;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
@@ -47,25 +47,27 @@ pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::calls::JsonrpcCallInfo;
use types::chat::FullChat;
use types::contact::{ContactObject, VcardContact};
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::notify_state::JsonrpcNotifyState;
use types::provider_info::ProviderInfo;
use types::reactions::JSONRPCReactions;
use types::reactions::JsonrpcReactions;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::{MessageInfo, MessageLoadResult};
use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
chat::{BasicChat, JsonrpcChatVisibility, MuteDuration},
location::JsonrpcLocation,
message::{
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
},
};
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::qr::QrObject;
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
#[derive(Debug)]
struct AccountState {
@@ -91,7 +93,8 @@ pub struct CommandApi {
/// Receiver side of the event channel.
///
/// Events from it can be received by calling `get_next_event` method.
/// Events from it can be received by calling
/// [`CommandApi::get_next_event`] method.
event_emitter: Arc<EventEmitter>,
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
@@ -123,7 +126,7 @@ impl CommandApi {
.read()
.await
.get_account(id)
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
Ok(sc)
}
@@ -173,7 +176,15 @@ impl CommandApi {
get_info()
}
/// Get the next event.
/// Get the next event, and remove it from the event queue.
///
/// If no events have happened since the last `get_next_event`
/// (i.e. if the event queue is empty), the response will be returned
/// only when a new event fires.
///
/// Note that if you are using the `BaseDeltaChat` JavaScript class
/// or the `Rpc` Python class, this function will be invoked
/// by those classes internally and should not be used manually.
async fn get_next_event(&self) -> Result<Event> {
self.event_emitter
.recv()
@@ -297,12 +308,17 @@ impl CommandApi {
Ok(Account::from_context(&ctx, account_id).await?)
} else {
Err(anyhow!(
"account with id {} doesn't exist anymore",
account_id
"account with id {account_id} doesn't exist anymore"
))
}
}
/// Get the current push notification state.
async fn get_push_state(&self, account_id: u32) -> Result<JsonrpcNotifyState> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.push_state().await.into())
}
/// Get the combined filesize of an account in bytes
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
let ctx = self.get_context(account_id).await?;
@@ -326,21 +342,10 @@ impl CommandApi {
/// instead of the domain.
async fn get_provider_info(
&self,
account_id: u32,
_account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let proxy_enabled = ctx
.get_config_bool(deltachat::config::Config::ProxyEnabled)
.await?;
let provider_info = get_provider_info(
&ctx,
email.split('@').next_back().unwrap_or(""),
proxy_enabled,
)
.await;
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
Ok(ProviderInfo::from_dc_type(provider_info))
}
@@ -383,11 +388,6 @@ impl CommandApi {
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
}
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.draft_self_report().await?.to_u32())
}
/// Sets the given configuration key.
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -886,6 +886,38 @@ impl CommandApi {
Ok(chat_id.to_u32())
}
/// Like `secure_join()`, but allows to pass a source and a UI-path.
/// You only need this if your UI has an option to send statistics
/// to Delta Chat's developers.
///
/// **source**: The source where the QR code came from.
/// E.g. a link that was clicked inside or outside Delta Chat,
/// the "Paste from Clipboard" action,
/// the "Load QR code as image" action,
/// or a QR code scan.
///
/// **uipath**: Which UI path did the user use to arrive at the QR code screen.
/// If the SecurejoinSource was ExternalLink or InternalLink,
/// pass `None` here, because the QR code screen wasn't even opened.
/// ```
async fn secure_join_with_ux_info(
&self,
account_id: u32,
qr: String,
source: Option<SecurejoinSource>,
uipath: Option<SecurejoinUiPath>,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let chat_id = securejoin::join_securejoin_with_ux_info(
&ctx,
&qr,
source.map(Into::into),
uipath.map(Into::into),
)
.await?;
Ok(chat_id.to_u32())
}
async fn leave_group(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::SELF).await
@@ -969,17 +1001,16 @@ impl CommandApi {
/// To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
/// This may be useful if you want to show some help for just created groups.
///
/// @param protect If set to 1 the function creates group with protection initially enabled.
/// Only verified members are allowed in these groups
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
/// `protect` argument is deprecated as of 2025-10-22 and is left for compatibility.
/// Pass `false` here.
async fn create_group_chat(
&self,
account_id: u32,
name: String,
_protect: bool,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_ex(&ctx, Some(protect), &name)
.await
.map(|id| id.to_u32())
chat::create_group(&ctx, &name).await.map(|id| id.to_u32())
}
/// Create a new unencrypted group chat.
@@ -988,7 +1019,7 @@ impl CommandApi {
/// address-contacts.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group_ex(&ctx, None, &name)
chat::create_group_unencrypted(&ctx, &name)
.await
.map(|id| id.to_u32())
}
@@ -999,7 +1030,7 @@ impl CommandApi {
.await
}
/// Create a new **broadcast channel**
/// Create a new, outgoing **broadcast channel**
/// (called "Channel" in the UI).
///
/// Broadcast channels are similar to groups on the sending device,
@@ -1060,7 +1091,7 @@ impl CommandApi {
&self,
account_id: u32,
chat_id: u32,
visibility: JSONRPCChatVisibility,
visibility: JsonrpcChatVisibility,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -1227,8 +1258,10 @@ impl CommandApi {
}
/// Returns all messages of a particular chat.
/// If `add_daymarker` is `true`, it will return them as
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
///
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
/// corresponding (following) day in the local timezone.
async fn get_message_ids(
&self,
account_id: u32,
@@ -1263,7 +1296,7 @@ impl CommandApi {
chat_id: u32,
info_only: bool,
add_daymarker: bool,
) -> Result<Vec<JSONRPCMessageListItem>> {
) -> Result<Vec<JsonrpcMessageListItem>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ex(
&ctx,
@@ -1277,7 +1310,7 @@ impl CommandApi {
Ok(msg
.iter()
.map(|chat_item| (*chat_item).into())
.collect::<Vec<JSONRPCMessageListItem>>())
.collect::<Vec<JsonrpcMessageListItem>>())
}
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
@@ -1471,7 +1504,14 @@ impl CommandApi {
/// Add a single contact as a result of an explicit user action.
///
/// Returns contact id of the created or existing contact
/// This will always create or look up an address-contact,
/// i.e. a contact identified by an email address,
/// with all messages sent to and from this contact being unencrypted.
/// If the user just clicked on an email address,
/// you should first check [`Self::lookup_contact_id_by_addr`]/`lookupContactIdByAddr.`,
/// and only if there is no contact yet, call this function here.
///
/// Returns contact id of the created or existing contact.
async fn create_contact(
&self,
account_id: u32,
@@ -1621,9 +1661,19 @@ impl CommandApi {
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
}
/// Check if an e-mail address belongs to a known and unblocked contact.
/// Looks up a known and unblocked contact with a given e-mail address.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
///
/// To validate an e-mail address independently of the contact database
/// use check_email_validity().
async fn lookup_contact_id_by_addr(
@@ -1779,13 +1829,13 @@ impl CommandApi {
/// Offers a backup for remote devices to retrieve.
///
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
/// Can be canceled by stopping the ongoing process. Success or failure can be tracked
/// via the `ImexProgress` event which should either reach `1000` for success or `0` for
/// failure.
///
/// This **stops IO** while it is running.
///
/// Returns once a remote device has retrieved the backup, or is cancelled.
/// Returns once a remote device has retrieved the backup, or is canceled.
async fn provide_backup(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -1851,7 +1901,7 @@ impl CommandApi {
/// This retrieves the backup from a remote device over the network and imports it into
/// the current device.
///
/// Can be cancelled by stopping the ongoing process.
/// Can be canceled by stopping the ongoing process.
///
/// Do not forget to call start_io on the account after a successful import,
/// otherwise it will not connect to the email server.
@@ -1889,7 +1939,7 @@ impl CommandApi {
/// If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
async fn get_connectivity(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_connectivity().await as u32)
Ok(ctx.get_connectivity() as u32)
}
/// Get an overview of the current connectivity, and possibly more statistics.
@@ -1972,6 +2022,11 @@ impl CommandApi {
Ok(())
}
/// Leaves the gossip of the webxdc with the given message id.
///
/// NB: When this is called before closing a webxdc app in UIs, it must be guaranteed that
/// `send_webxdc_realtime_*()` functions aren't called for the given `instance_message_id`
/// anymore until the app is open again.
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await
@@ -2049,6 +2104,53 @@ impl CommandApi {
.map(|msg_id| msg_id.to_u32()))
}
/// Starts an outgoing call.
async fn place_outgoing_call(
&self,
account_id: u32,
chat_id: u32,
place_call_info: String,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let msg_id = ctx
.place_outgoing_call(ChatId::new(chat_id), place_call_info)
.await?;
Ok(msg_id.to_u32())
}
/// Accepts an incoming call.
async fn accept_incoming_call(
&self,
account_id: u32,
msg_id: u32,
accept_call_info: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.accept_incoming_call(MsgId::new(msg_id), accept_call_info)
.await?;
Ok(())
}
/// Ends incoming or outgoing call.
async fn end_call(&self, account_id: u32, msg_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.end_call(MsgId::new(msg_id)).await?;
Ok(())
}
/// Returns information about the call.
async fn call_info(&self, account_id: u32, msg_id: u32) -> Result<JsonrpcCallInfo> {
let ctx = self.get_context(account_id).await?;
let call_info = JsonrpcCallInfo::from_msg_id(&ctx, MsgId::new(msg_id)).await?;
Ok(call_info)
}
/// Returns JSON with ICE servers, to be used for WebRTC video calls.
async fn ice_servers(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ice_servers(&ctx).await
}
/// Makes an HTTP GET request and returns a response.
///
/// `url` is the HTTP or HTTPS URL.
@@ -2129,7 +2231,7 @@ impl CommandApi {
&self,
account_id: u32,
message_id: u32,
) -> Result<Option<JSONRPCReactions>> {
) -> Result<Option<JsonrpcReactions>> {
let ctx = self.get_context(account_id).await?;
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
if reactions.is_empty() {
@@ -2200,13 +2302,6 @@ impl CommandApi {
}
}
async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
}
// ---------------------------------------------
// misc prototyping functions
// that might get removed later again
@@ -2237,8 +2332,7 @@ impl CommandApi {
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
ensure!(
message.get_viewtype() == Viewtype::Sticker,
"message {} is not a sticker",
msg_id
"message {msg_id} is not a sticker"
);
let account_folder = ctx
.get_dbfile()
@@ -2458,10 +2552,7 @@ impl CommandApi {
.to_u32();
Ok(msg_id)
} else {
Err(anyhow!(
"chat with id {} doesn't have draft message",
chat_id
))
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
}
}
}

View File

@@ -32,7 +32,10 @@ impl Account {
let addr = ctx.get_config(Config::Addr).await?;
let profile_image = ctx.get_config(Config::Selfavatar).await?;
let color = color_int_to_hex_string(
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
Contact::get_by_id(ctx, ContactId::SELF)
.await?
.get_or_gen_color(ctx)
.await?,
);
let private_tag = ctx.get_config(Config::PrivateTag).await?;
Ok(Account::Configured {

View File

@@ -0,0 +1,97 @@
use anyhow::{Context as _, Result};
use deltachat::calls::{call_state, sdp_has_video, CallState};
use deltachat::context::Context;
use deltachat::message::MsgId;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "CallInfo", rename_all = "camelCase")]
pub struct JsonrpcCallInfo {
/// SDP offer.
///
/// Can be used to manually answer the call
/// even if incoming call event was missed.
pub sdp_offer: String,
/// True if SDP offer has a video.
pub has_video: bool,
/// Call state.
///
/// For example, if the call is accepted, active, canceled, declined etc.
pub state: JsonrpcCallState,
}
impl JsonrpcCallInfo {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallInfo> {
let call_info = context.load_call_by_id(msg_id).await?.with_context(|| {
format!("Attempting to get call state of non-call message {msg_id}")
})?;
let sdp_offer = call_info.place_call_info.clone();
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
Ok(JsonrpcCallInfo {
sdp_offer,
has_video,
state,
})
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "CallState", tag = "kind")]
pub enum JsonrpcCallState {
/// Fresh incoming or outgoing call that is still ringing.
///
/// There is no separate state for outgoing call
/// that has been dialled but not ringing on the other side yet
/// as we don't know whether the other side received our call.
Alerting,
/// Active call.
Active,
/// Completed call that was once active
/// and then was terminated for any reason.
Completed {
/// Call duration in seconds.
duration: i64,
},
/// Incoming call that was not picked up within a timeout
/// or was explicitly ended by the caller before we picked up.
Missed,
/// Incoming call that was explicitly ended on our side
/// before picking up or outgoing call
/// that was declined before the timeout.
Declined,
/// Outgoing call that has been canceled on our side
/// before receiving a response.
///
/// Incoming calls cannot be canceled,
/// on the receiver side canceled calls
/// usually result in missed calls.
Canceled,
}
impl JsonrpcCallState {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallState> {
let call_state = call_state(context, msg_id).await?;
let jsonrpc_call_state = match call_state {
CallState::Alerting => JsonrpcCallState::Alerting,
CallState::Active => JsonrpcCallState::Active,
CallState::Completed { duration } => JsonrpcCallState::Completed { duration },
CallState::Missed => JsonrpcCallState::Missed,
CallState::Declined => JsonrpcCallState::Declined,
CallState::Canceled => JsonrpcCallState::Canceled,
};
Ok(jsonrpc_call_state)
}
}

View File

@@ -6,7 +6,6 @@ use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};
use deltachat::context::Context;
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
@@ -19,17 +18,6 @@ pub struct FullChat {
id: u32,
name: String,
/// True if the chat is protected.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
@@ -57,7 +45,7 @@ pub struct FullChat {
archived: bool,
pinned: bool,
// subtitle - will be moved to frontend because it uses translation functions
chat_type: u32,
chat_type: JsonrpcChatType,
is_unpromoted: bool,
is_self_talk: bool,
contacts: Vec<ContactObject>,
@@ -70,8 +58,15 @@ pub struct FullChat {
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool,
/// Note that this is different from
/// [`ChatListItem::is_self_in_group`](`crate::api::types::chat_list::ChatListItemFetchResult::ChatListItem::is_self_in_group`).
/// This property should only be accessed
/// when [`FullChat::chat_type`] is [`Chattype::Group`].
//
// We could utilize [`Chat::is_self_in_chat`],
// but that would be an extra DB query.
self_in_group: bool,
is_muted: bool,
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
@@ -130,12 +125,11 @@ impl FullChat {
Ok(FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_type: chat.get_type().into(),
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
@@ -144,7 +138,6 @@ impl FullChat {
color,
fresh_message_counter,
is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(),
self_in_group: contact_ids.contains(&ContactId::SELF),
is_muted: chat.is_muted(),
@@ -172,18 +165,6 @@ pub struct BasicChat {
id: u32,
name: String,
/// True if the chat is protected.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
@@ -210,12 +191,12 @@ pub struct BasicChat {
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
chat_type: u32,
chat_type: JsonrpcChatType,
is_unpromoted: bool,
is_self_talk: bool,
color: String,
is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool,
is_muted: bool,
}
@@ -234,17 +215,15 @@ impl BasicChat {
Ok(BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_type: chat.get_type().into(),
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
color,
is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(),
is_muted: chat.is_muted(),
})
@@ -279,18 +258,52 @@ impl MuteDuration {
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "ChatVisibility")]
pub enum JSONRPCChatVisibility {
pub enum JsonrpcChatVisibility {
Normal,
Archived,
Pinned,
}
impl JSONRPCChatVisibility {
impl JsonrpcChatVisibility {
pub fn into_core_type(self) -> ChatVisibility {
match self {
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
JsonrpcChatVisibility::Normal => ChatVisibility::Normal,
JsonrpcChatVisibility::Archived => ChatVisibility::Archived,
JsonrpcChatVisibility::Pinned => ChatVisibility::Pinned,
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, TypeDef, schemars::JsonSchema)]
#[serde(rename = "ChatType")]
pub enum JsonrpcChatType {
Single,
Group,
Mailinglist,
OutBroadcast,
InBroadcast,
}
impl From<Chattype> for JsonrpcChatType {
fn from(chattype: Chattype) -> Self {
match chattype {
Chattype::Single => JsonrpcChatType::Single,
Chattype::Group => JsonrpcChatType::Group,
Chattype::Mailinglist => JsonrpcChatType::Mailinglist,
Chattype::OutBroadcast => JsonrpcChatType::OutBroadcast,
Chattype::InBroadcast => JsonrpcChatType::InBroadcast,
}
}
}
impl From<JsonrpcChatType> for Chattype {
fn from(chattype: JsonrpcChatType) -> Self {
match chattype {
JsonrpcChatType::Single => Chattype::Single,
JsonrpcChatType::Group => Chattype::Group,
JsonrpcChatType::Mailinglist => Chattype::Mailinglist,
JsonrpcChatType::OutBroadcast => Chattype::OutBroadcast,
JsonrpcChatType::InBroadcast => Chattype::InBroadcast,
}
}
}

View File

@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
use deltachat::chat::{Chat, ChatId};
use deltachat::chatlist::get_last_message_for_chat;
use deltachat::constants::*;
use deltachat::contact::{Contact, ContactId};
use deltachat::contact::Contact;
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
@@ -11,6 +11,7 @@ use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::chat::JsonrpcChatType;
use super::color_int_to_hex_string;
use super::message::MessageViewtype;
@@ -23,14 +24,13 @@ pub enum ChatListItemFetchResult {
name: String,
avatar_path: Option<String>,
color: String,
chat_type: u32,
chat_type: JsonrpcChatType,
last_updated: Option<i64>,
summary_text1: String,
summary_text2: String,
summary_status: u32,
/// showing preview if last chat message is image
summary_preview_image: Option<String>,
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
@@ -127,11 +127,8 @@ pub(crate) async fn get_chat_list_item_by_id(
None => (None, None),
};
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
let self_in_group = chat_contacts.contains(&ContactId::SELF);
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
let contact = chat_contacts.first();
let was_seen_recently = match contact {
Some(contact) => Contact::get_by_id(ctx, *contact)
@@ -155,19 +152,18 @@ pub(crate) async fn get_chat_list_item_by_id(
name: chat.get_name().to_owned(),
avatar_path,
color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_type: chat.get_type().into(),
last_updated,
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(ctx).await?,
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
is_self_talk: chat.is_self_talk(),
is_device_talk: chat.is_device_talk(),
is_self_in_group: self_in_group,
is_self_in_group: chat.is_self_in_chat(ctx).await?,
is_sending_location: chat.is_sending_locations(),
is_archived: visibility == ChatVisibility::Archived,
is_pinned: visibility == ChatVisibility::Pinned,

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use deltachat::color;
use deltachat::context::Context;
use deltachat::key::{DcKey, SignedPublicKey};
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -31,27 +31,36 @@ pub struct ContactObject {
/// e.g. if we just scanned the fingerprint from a QR code.
e2ee_avail: bool,
/// True if the contact can be added to verified groups.
/// True if the contact
/// can be added to protected chats
/// because SELF and contact have verified their fingerprints in both directions.
///
/// If this is true
/// UI should display green checkmark after the contact name
/// in contact list items,
/// in chat member list items
/// and in profiles if no chat with the contact exist.
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
is_verified: bool,
/// True if the contact profile title should have a green checkmark.
/// The contact ID that verified a contact.
///
/// This indicates whether 1:1 chat has a green checkmark
/// or will have a green checkmark if created.
is_profile_verified: bool,
/// The ID of the contact that verified this contact.
/// As verifier may be unknown,
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
///
/// If this is present,
/// display a green checkmark and "Introduced by ..."
/// string followed by the verifier contact name and address
/// in the contact profile.
/// UI should display the information in the contact's profile as follows:
///
/// - If `verifierId` != 0,
/// display text "Introduced by ..."
/// with the name and address of the contact
/// formatted by `name_and_addr`/`nameAndAddr`.
/// Prefix the text by a green checkmark.
///
/// - If `verifierId` == 0 and `isVerified` != 0,
/// display "Introduced" prefixed by a green checkmark.
///
/// - if `verifierId` == 0 and `isVerified` == 0,
/// display nothing
///
/// This contains the contact ID of the verifier.
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
/// If it is None/Null, we don't have verifier information or
/// the contact is not verified.
verifier_id: Option<u32>,
/// the contact's last seen timestamp
@@ -72,7 +81,6 @@ impl ContactObject {
None => None,
};
let is_verified = contact.is_verified(context).await?;
let is_profile_verified = contact.is_profile_verified(context).await?;
let verifier_id = contact
.get_verifier_id(context)
@@ -94,7 +102,6 @@ impl ContactObject {
is_key_contact: contact.is_key_contact(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
is_profile_verified,
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
@@ -123,7 +130,13 @@ pub struct VcardContact {
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
let display_name = vc.display_name().to_string();
let color = color::str_to_color(&vc.addr.to_lowercase());
let is_self = false;
let fpr = vc.key.as_deref().and_then(|k| {
SignedPublicKey::from_base64(k)
.ok()
.map(|k| k.dc_fingerprint())
});
let color = deltachat::contact::get_color(is_self, &vc.addr, &fpr);
Self {
addr: vc.addr,
display_name,

View File

@@ -2,6 +2,8 @@ use deltachat::{Event as CoreEvent, EventType as CoreEventType};
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::chat::JsonrpcChatType;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Event {
@@ -224,7 +226,6 @@ pub enum EventType {
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See setChatName(), setChatProfileImage(), addContactToChat()
/// and removeContactFromChat().
///
@@ -294,8 +295,8 @@ pub enum EventType {
#[serde(rename_all = "camelCase")]
ImexFileWritten { path: String },
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
/// Progress event sent when SecureJoin protocol has finished
/// from the view of the inviter (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by getChatSecurejoinQrCodeSvg().
@@ -304,11 +305,14 @@ pub enum EventType {
/// ID of the contact that wants to join.
contact_id: u32,
/// Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
/// The type of the joined chat.
/// This can take the same values
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
chat_type: JsonrpcChatType,
/// ID of the chat in case of success.
chat_id: u32,
/// Progress, always 1000.
progress: usize,
},
@@ -417,6 +421,45 @@ pub enum EventType {
/// Number of events skipped.
n: u64,
},
/// Incoming call.
IncomingCall {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
/// User-defined info as passed to place_outgoing_call()
place_call_info: String,
/// True if incoming call is a video call.
has_video: bool,
},
/// Incoming call accepted.
/// This is esp. interesting to stop ringing on other devices.
IncomingCallAccepted {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
},
/// Outgoing call accepted.
OutgoingCallAccepted {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
/// User-defined info passed to dc_accept_incoming_call(
accept_call_info: String,
},
/// Call ended.
CallEnded {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
},
}
impl From<CoreEventType> for EventType {
@@ -523,9 +566,13 @@ impl From<CoreEventType> for EventType {
},
CoreEventType::SecurejoinInviterProgress {
contact_id,
chat_type,
chat_id,
progress,
} => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(),
chat_type: chat_type.into(),
chat_id: chat_id.to_u32(),
progress,
},
CoreEventType::SecurejoinJoinerProgress {
@@ -567,6 +614,34 @@ impl From<CoreEventType> for EventType {
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
CoreEventType::AccountsChanged => AccountsChanged,
CoreEventType::AccountsItemChanged => AccountsItemChanged,
CoreEventType::IncomingCall {
msg_id,
chat_id,
place_call_info,
has_video,
} => IncomingCall {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
place_call_info,
has_video,
},
CoreEventType::IncomingCallAccepted { msg_id, chat_id } => IncomingCallAccepted {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
CoreEventType::OutgoingCallAccepted {
msg_id,
chat_id,
accept_call_info,
} => OutgoingCallAccepted {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
accept_call_info,
},
CoreEventType::CallEnded { msg_id, chat_id } => CallEnded {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),

View File

@@ -16,9 +16,10 @@ use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::chat::JsonrpcChatType;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::reactions::JsonrpcReactions;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "kind")]
@@ -84,9 +85,6 @@ pub struct MessageObject {
dimensions_height: i32,
dimensions_width: i32,
videochat_type: Option<u32>,
videochat_url: Option<String>,
override_sender_name: Option<String>,
sender: ContactObject,
@@ -105,7 +103,7 @@ pub struct MessageObject {
saved_message_id: Option<u32>,
reactions: Option<JSONRPCReactions>,
reactions: Option<JsonrpcReactions>,
vcard_contact: Option<VcardContact>,
}
@@ -239,15 +237,6 @@ impl MessageObject {
dimensions_height: message.get_height(),
dimensions_width: message.get_width(),
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
.context("videochat type conversion to number failed")?,
),
None => None,
},
videochat_url: message.get_videochat_url(),
override_sender_name,
sender,
@@ -321,8 +310,8 @@ pub enum MessageViewtype {
/// Message containing any file, eg. a PDF.
File,
/// Message is an invitation to a videochat.
VideochatInvitation,
/// Message is a call.
Call,
/// Message is an webxdc instance.
Webxdc,
@@ -345,7 +334,7 @@ impl From<Viewtype> for MessageViewtype {
Viewtype::Voice => MessageViewtype::Voice,
Viewtype::Video => MessageViewtype::Video,
Viewtype::File => MessageViewtype::File,
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
Viewtype::Call => MessageViewtype::Call,
Viewtype::Webxdc => MessageViewtype::Webxdc,
Viewtype::Vcard => MessageViewtype::Vcard,
}
@@ -364,7 +353,7 @@ impl From<MessageViewtype> for Viewtype {
MessageViewtype::Voice => Viewtype::Voice,
MessageViewtype::Video => Viewtype::Video,
MessageViewtype::File => Viewtype::File,
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
MessageViewtype::Call => Viewtype::Call,
MessageViewtype::Webxdc => Viewtype::Webxdc,
MessageViewtype::Vcard => Viewtype::Vcard,
}
@@ -437,6 +426,9 @@ pub enum SystemMessageType {
/// This message contains a users iroh node address.
IrohNodeAddr,
CallAccepted,
CallEnded,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
@@ -463,6 +455,8 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
SystemMessage::CallEnded => SystemMessageType::CallEnded,
}
}
}
@@ -538,8 +532,7 @@ pub struct MessageSearchResult {
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
chat_type: u32,
is_chat_protected: bool,
chat_type: JsonrpcChatType,
is_chat_contact_request: bool,
is_chat_archived: bool,
message: String,
@@ -577,9 +570,8 @@ impl MessageSearchResult {
chat_id: chat.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_type: chat.get_type().into(),
chat_profile_image,
is_chat_protected: chat.is_protected(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
message: message.get_text(),
@@ -590,7 +582,7 @@ impl MessageSearchResult {
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
pub enum JSONRPCMessageListItem {
pub enum JsonrpcMessageListItem {
Message {
msg_id: u32,
},
@@ -603,13 +595,13 @@ pub enum JSONRPCMessageListItem {
},
}
impl From<ChatItem> for JSONRPCMessageListItem {
impl From<ChatItem> for JsonrpcMessageListItem {
fn from(item: ChatItem) -> Self {
match item {
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
ChatItem::Message { msg_id } => JsonrpcMessageListItem::Message {
msg_id: msg_id.to_u32(),
},
ChatItem::DayMarker { timestamp } => JSONRPCMessageListItem::DayMarker { timestamp },
ChatItem::DayMarker { timestamp } => JsonrpcMessageListItem::DayMarker { timestamp },
}
}
}

View File

@@ -1,4 +1,5 @@
pub mod account;
pub mod calls;
pub mod chat;
pub mod chat_list;
pub mod contact;
@@ -7,6 +8,7 @@ pub mod http;
pub mod location;
pub mod login_param;
pub mod message;
pub mod notify_state;
pub mod provider_info;
pub mod qr;
pub mod reactions;

View File

@@ -0,0 +1,26 @@
use deltachat::push::NotifyState;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "NotifyState")]
pub enum JsonrpcNotifyState {
/// Not subscribed to push notifications.
NotConnected,
/// Subscribed to heartbeat push notifications.
Heartbeat,
/// Subscribed to push notifications for new messages.
Connected,
}
impl From<NotifyState> for JsonrpcNotifyState {
fn from(state: NotifyState) -> Self {
match state {
NotifyState::NotConnected => Self::NotConnected,
NotifyState::Heartbeat => Self::Heartbeat,
NotifyState::Connected => Self::Connected,
}
}
}

View File

@@ -1,4 +1,5 @@
use deltachat::qr::Qr;
use serde::Deserialize;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -34,6 +35,26 @@ pub enum QrObject {
/// Authentication code.
authcode: String,
},
/// Ask the user whether to join the broadcast channel.
AskJoinBroadcast {
/// The user-visible name of this broadcast channel
name: String,
/// A string of random characters,
/// uniquely identifying this broadcast channel across all databases/clients.
/// Called `grpid` for historic reasons:
/// The id of multi-user chats is always called `grpid` in the database
/// because groups were once the only multi-user chats.
grpid: String,
/// ID of the contact who owns the broadcast channel and created the QR code.
contact_id: u32,
/// Fingerprint of the broadcast channel owner's key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
@@ -207,6 +228,25 @@ impl From<Qr> for QrObject {
authcode,
}
}
Qr::AskJoinBroadcast {
name,
grpid,
contact_id,
fingerprint,
authcode,
invitenumber,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskJoinBroadcast {
name,
grpid,
contact_id,
fingerprint,
authcode,
invitenumber,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
@@ -225,13 +265,6 @@ impl From<Qr> for QrObject {
auth_token,
},
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
@@ -311,3 +344,53 @@ impl From<Qr> for QrObject {
}
}
}
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
pub enum SecurejoinSource {
/// Because of some problem, it is unknown where the QR code came from.
Unknown,
/// The user opened a link somewhere outside Delta Chat
ExternalLink,
/// The user clicked on a link in a message inside Delta Chat
InternalLink,
/// The user clicked "Paste from Clipboard" in the QR scan activity
Clipboard,
/// The user clicked "Load QR code as image" in the QR scan activity
ImageLoaded,
/// The user scanned a QR code
Scan,
}
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
pub enum SecurejoinUiPath {
/// The UI path is unknown, or the user didn't open the QR code screen at all.
Unknown,
/// The user directly clicked on the QR icon in the main screen
QrIcon,
/// The user first clicked on the `+` button in the main screen,
/// and then on "New Contact"
NewContact,
}
impl From<SecurejoinSource> for deltachat::SecurejoinSource {
fn from(value: SecurejoinSource) -> Self {
match value {
SecurejoinSource::Unknown => deltachat::SecurejoinSource::Unknown,
SecurejoinSource::ExternalLink => deltachat::SecurejoinSource::ExternalLink,
SecurejoinSource::InternalLink => deltachat::SecurejoinSource::InternalLink,
SecurejoinSource::Clipboard => deltachat::SecurejoinSource::Clipboard,
SecurejoinSource::ImageLoaded => deltachat::SecurejoinSource::ImageLoaded,
SecurejoinSource::Scan => deltachat::SecurejoinSource::Scan,
}
}
}
impl From<SecurejoinUiPath> for deltachat::SecurejoinUiPath {
fn from(value: SecurejoinUiPath) -> Self {
match value {
SecurejoinUiPath::Unknown => deltachat::SecurejoinUiPath::Unknown,
SecurejoinUiPath::QrIcon => deltachat::SecurejoinUiPath::QrIcon,
SecurejoinUiPath::NewContact => deltachat::SecurejoinUiPath::NewContact,
}
}
}

View File

@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
/// A single reaction emoji.
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Reaction", rename_all = "camelCase")]
pub struct JSONRPCReaction {
pub struct JsonrpcReaction {
/// Emoji.
emoji: String,
@@ -22,14 +22,14 @@ pub struct JSONRPCReaction {
/// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions {
pub struct JsonrpcReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count, sorted in descending order.
reactions: Vec<JSONRPCReaction>,
reactions: Vec<JsonrpcReaction>,
}
impl From<Reactions> for JSONRPCReactions {
impl From<Reactions> for JsonrpcReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
@@ -56,7 +56,7 @@ impl From<Reactions> for JSONRPCReactions {
false
};
let reaction = JSONRPCReaction {
let reaction = JsonrpcReaction {
emoji,
count,
is_from_self,
@@ -64,7 +64,7 @@ impl From<Reactions> for JSONRPCReactions {
reactions_v.push(reaction)
}
JSONRPCReactions {
JsonrpcReactions {
reactions_by_contact,
reactions: reactions_v,
}

View File

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

View File

@@ -40,15 +40,35 @@ const constants = data
key.startsWith("DC_DOWNLOAD") ||
key.startsWith("DC_INFO_") ||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
key.startsWith("DC_QR_")
key.startsWith("DC_QR_") ||
key.startsWith("DC_CERTCK_") ||
key.startsWith("DC_SOCKET_") ||
key.startsWith("DC_LP_AUTH_") ||
key.startsWith("DC_PUSH_") ||
key.startsWith("DC_TEXT1_") ||
key.startsWith("DC_CHAT_TYPE")
);
})
.map((row) => {
return ` ${row.key}: ${row.value}`;
return ` export const ${row.key} = ${row.value};`;
})
.join(",\n");
.join("\n");
writeFileSync(
resolve(__dirname, "../generated/constants.ts"),
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
`// Generated!
export namespace C {
${constants}
/** @deprecated 10-8-2025 compare string directly with \`== "Group"\` */
export const DC_CHAT_TYPE_GROUP = "Group";
/** @deprecated 10-8-2025 compare string directly with \`== "InBroadcast"\`*/
export const DC_CHAT_TYPE_IN_BROADCAST = "InBroadcast";
/** @deprecated 10-8-2025 compare string directly with \`== "Mailinglist"\` */
export const DC_CHAT_TYPE_MAILINGLIST = "Mailinglist";
/** @deprecated 10-8-2025 compare string directly with \`== "OutBroadcast"\` */
export const DC_CHAT_TYPE_OUT_BROADCAST = "OutBroadcast";
/** @deprecated 10-8-2025 compare string directly with \`== "Single"\` */
export const DC_CHAT_TYPE_SINGLE = "Single";
}\n`,
);

View File

@@ -28,7 +28,6 @@ export class BaseDeltaChat<
Transport extends BaseTransport<any>,
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
//@ts-ignore
@@ -36,6 +35,10 @@ export class BaseDeltaChat<
constructor(
public transport: Transport,
/**
* Whether to start calling {@linkcode RawClient.getNextEvent}
* and emitting the respective events on this class.
*/
startEventLoop: boolean,
) {
super();
@@ -45,6 +48,9 @@ export class BaseDeltaChat<
}
}
/**
* @see the constructor's `startEventLoop`
*/
async eventLoop(): Promise<void> {
while (true) {
const event = await this.rpc.getNextEvent();
@@ -63,10 +69,17 @@ export class BaseDeltaChat<
}
}
/**
* @deprecated use {@linkcode BaseDeltaChat.rpc.getAllAccounts} instead.
*/
async listAccounts(): Promise<T.Account[]> {
return await this.rpc.getAllAccounts();
}
/**
* A convenience function to listen on events binned by `account_id`
* (see {@linkcode RawClient.getAllAccounts}).
*/
getContextEvents(account_id: number) {
if (this.contextEmitters[account_id]) {
return this.contextEmitters[account_id];

View File

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

View File

@@ -6,9 +6,7 @@ use std::str::FromStr;
use std::time::Duration;
use anyhow::{bail, ensure, Result};
use deltachat::chat::{
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
};
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
@@ -72,11 +70,6 @@ async fn reset_tables(context: &Context, bits: i32) {
.await
.unwrap();
context.sql().config_cache().write().await.clear();
context
.sql()
.execute("DELETE FROM leftgrps;", ())
.await
.unwrap();
println!("(8) Rest but server config reset.");
}
@@ -210,13 +203,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else if msg.get_viewtype() == Viewtype::Webxdc {
if msg.get_viewtype() == Viewtype::Webxdc {
match msg.get_webxdc_info(context).await {
Ok(info) => format!(
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
@@ -353,7 +340,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
createchat <contact-id>\n\
creategroup <name>\n\
createbroadcast <name>\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
@@ -364,6 +350,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
send-sync <text>\n\
sendempty\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
@@ -371,7 +358,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
sendhtml <file for html-part> [<text for plain-part>]\n\
sendsyncmsg\n\
sendupdate <msg-id> <json status update>\n\
videochat\n\
draft [<text>]\n\
devicemsg <text>\n\
listmedia\n\
@@ -403,6 +389,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
block <contact-id>\n\
unblock <contact-id>\n\
listblocked\n\
import-vcard <file>\n\
make-vcard <file> <contact-id> [contact-id ...]\n\
======================================Misc.==\n\
getqr [<chat-id>]\n\
getqrsvg [<chat-id>]\n\
@@ -423,7 +411,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Ok(setup_code) => {
println!("Setup code for the transferred setup message: {setup_code}",)
}
Err(err) => bail!("Failed to generate setup code: {}", err),
Err(err) => bail!("Failed to generate setup code: {err}"),
},
"get-setupcodebegin" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
@@ -437,7 +425,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
setupcodebegin.unwrap_or_default(),
);
} else {
bail!("{} is no setup message.", msg_id,);
bail!("{msg_id} is no setup message.",);
}
}
"continue-key-transfer" => {
@@ -532,7 +520,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Report written to: {file:#?}");
}
Err(err) => {
bail!("Failed to get connectivity html: {}", err);
bail!("Failed to get connectivity html: {err}");
}
}
}
@@ -567,7 +555,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
println!(
"{}#{}: {} [{} fresh] {}{}{}{}",
"{}#{}: {} [{} fresh] {}{}{}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
@@ -578,7 +566,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
if chat.is_contact_request() {
"🆕"
} else {
@@ -693,7 +680,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{}{} {}",
"{}#{}: {} [{}]{}{}{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
@@ -711,11 +698,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
@@ -744,8 +726,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
let chat_id = chat::create_group(&context, arg1).await?;
println!("Group#{chat_id} created successfully.");
}
@@ -755,13 +736,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Broadcast#{chat_id} created successfully.");
}
"createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
println!("Group#{chat_id} created and protected successfully.");
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
@@ -913,6 +887,23 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
}
"send-sync" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No message text given.");
// Send message over a dedicated SMTP connection
// and measure time.
//
// This can be used to benchmark SMTP connection establishment.
let time_start = std::time::Instant::now();
let msg = format!("{arg1} {arg2}");
let mut msg = Message::new_text(msg);
chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
let time_needed = time_start.elapsed();
println!("Sent message in {time_needed:?}.");
}
"sendempty" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
@@ -960,10 +951,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let msg_id = MsgId::new(arg1.parse()?);
context.send_webxdc_status_update(msg_id, arg2).await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
}
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");
@@ -1218,6 +1205,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
log_contactlist(&context, &contacts).await?;
println!("{} blocked contacts.", contacts.len());
}
"import-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
let contacts = import_vcard(&context, &vcard_content).await?;
println!("vCard contacts imported:");
log_contactlist(&context, &contacts).await?;
}
"make-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
let mut contact_ids = vec![];
for x in arg2.split_whitespace() {
contact_ids.push(ContactId::new(x.parse()?))
}
let vcard_content = make_vcard(&context, &contact_ids).await?;
fs::write(&arg1.to_string(), vcard_content).await?;
println!("vCard written to: {arg1}");
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let qr = check_qr(&context, arg1).await?;
@@ -1239,10 +1244,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
let proxy_enabled = context
.get_config_bool(config::Config::ProxyEnabled)
.await?;
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);
@@ -1278,7 +1280,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
}
"" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
}
Ok(())

View File

@@ -199,6 +199,7 @@ const CHAT_COMMANDS: [&str; 39] = [
"dellocations",
"getlocations",
"send",
"send-sync",
"sendempty",
"sendimage",
"sendsticker",
@@ -206,7 +207,6 @@ const CHAT_COMMANDS: [&str; 39] = [
"sendhtml",
"sendsyncmsg",
"sendupdate",
"videochat",
"draft",
"devicemsg",
"listmedia",
@@ -232,7 +232,7 @@ const MESSAGE_COMMANDS: [&str; 10] = [
"delmsg",
"react",
];
const CONTACT_COMMANDS: [&str; 7] = [
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
"addcontact",
"contactinfo",
@@ -240,6 +240,8 @@ const CONTACT_COMMANDS: [&str; 7] = [
"block",
"unblock",
"listblocked",
"import-vcard",
"make-vcard",
];
const MISC_COMMANDS: [&str; 14] = [
"getqr",
@@ -465,7 +467,7 @@ async fn handle_cmd(
println!("QR code svg written to: {file:#?}");
}
Err(err) => {
bail!("Failed to get QR code svg: {}", err);
bail!("Failed to get QR code svg: {err}");
}
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.4.0"
version = "2.26.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from warnings import warn
@@ -124,6 +125,11 @@ class Account:
"""Add a new transport."""
yield self._rpc.add_or_update_transport.future(self.id, params)
@futuremethod
def add_transport_from_qr(self, qr: str):
"""Add a new transport using a QR code."""
yield self._rpc.add_transport_from_qr.future(self.id, qr)
@futuremethod
def list_transports(self):
"""Return the list of all email accounts that are used as a transport in the current profile."""
@@ -185,7 +191,21 @@ class Account:
return Contact(self, contact_id)
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Check if an e-mail address belongs to a known and unblocked contact."""
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
@@ -285,7 +305,7 @@ class Account:
chats.append(AttrDict(item))
return chats
def create_group(self, name: str, protect: bool = False) -> Chat:
def create_group(self, name: str) -> Chat:
"""Create a new group chat.
After creation,
@@ -302,15 +322,11 @@ class Account:
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
(see `get_full_snapshot()` / `get_basic_snapshot()`).
This may be useful if you want to show some help for just created groups.
:param protect: If set to 1 the function creates group with protection initially enabled.
Only verified members are allowed in these groups
and end-to-end-encryption is always enabled.
"""
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
return Chat(self, self._rpc.create_group_chat(self.id, name, False))
def create_broadcast(self, name: str) -> Chat:
"""Create a new **broadcast channel**
"""Create a new, outgoing **broadcast channel**
(called "Channel" in the UI).
Broadcast channels are similar to groups on the sending device,
@@ -383,9 +399,10 @@ class Account:
next_msg_ids = self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
@futuremethod
def wait_next_messages(self) -> list[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = self._rpc.wait_next_msgs(self.id)
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def wait_for_incoming_msg_event(self):
@@ -400,12 +417,21 @@ class Account:
"""Wait for messages noticed event and return it."""
return self.wait_for_event(EventType.MSGS_NOTICED)
def wait_for_msg(self, event_type) -> Message:
"""Wait for an event about the message.
Consumes all events before the matching event.
Returns a message corresponding to the msg_id field of the event.
"""
event = self.wait_for_event(event_type)
return self.get_message_by_id(event.msg_id)
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
Consumes all events before the next incoming message event.
"""
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
return self.wait_for_msg(EventType.INCOMING_MSG)
def wait_for_securejoin_inviter_success(self):
"""Wait until SecureJoin process finishes successfully on the inviter side."""
@@ -456,3 +482,8 @@ class Account:
def initiate_autocrypt_key_transfer(self) -> None:
"""Send Autocrypt Setup Message."""
return self._rpc.initiate_autocrypt_key_transfer(self.id)
def ice_servers(self) -> list:
"""Return ICE servers for WebRTC configuration."""
ice_servers_json = self._rpc.ice_servers(self.id)
return json.loads(ice_servers_json)

View File

@@ -168,6 +168,11 @@ class Chat:
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
def resend_messages(self, messages: list[Message]) -> None:
"""Resend a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
self._rpc.resend_messages(self.account.id, msg_ids)
def forward_messages(self, messages: list[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
@@ -289,3 +294,8 @@ class Chat:
f.write(vcard.encode())
f.flush()
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
def place_outgoing_call(self, place_call_info: str) -> Message:
"""Starts an outgoing call."""
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
return Message(self.account, msg_id)

View File

@@ -83,11 +83,10 @@ class Client:
def configure(self, email: str, password: str, **kwargs) -> None:
"""Configure the client."""
self.account.set_config("addr", email)
self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
self.account.set_config(key, value)
self.account.configure()
params = {"addr": email, "password": password}
self.account.add_or_update_transport(params)
self.logger.debug("Account configured")
def run_forever(self) -> None:

View File

@@ -73,6 +73,10 @@ class EventType(str, Enum):
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
ACCOUNTS_CHANGED = "AccountsChanged"
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
INCOMING_CALL = "IncomingCall"
INCOMING_CALL_ACCEPTED = "IncomingCallAccepted"
OUTGOING_CALL_ACCEPTED = "OutgoingCallAccepted"
CALL_ENDED = "CallEnded"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
@@ -87,19 +91,17 @@ class ChatId(IntEnum):
LAST_SPECIAL = 9
class ChatType(IntEnum):
class ChatType(str, Enum):
"""Chat type."""
UNDEFINED = 0
SINGLE = 100
SINGLE = "Single"
"""1:1 chat, i.e. a direct chat with a single contact"""
GROUP = 120
GROUP = "Group"
MAILINGLIST = 140
MAILINGLIST = "Mailinglist"
OUT_BROADCAST = 160
OUT_BROADCAST = "OutBroadcast"
"""Outgoing broadcast channel, called "Channel" in the UI.
The user can send into this channel,
@@ -111,7 +113,7 @@ class ChatType(IntEnum):
which would make it hard to grep for it.
"""
IN_BROADCAST = 165
IN_BROADCAST = "InBroadcast"
"""Incoming broadcast channel, called "Channel" in the UI.
This channel is read-only,
@@ -156,7 +158,6 @@ class ViewType(str, Enum):
VOICE = "Voice"
VIDEO = "Video"
FILE = "File"
VIDEOCHAT_INVITATION = "VideochatInvitation"
WEBXDC = "Webxdc"
VCARD = "Vcard"
@@ -275,11 +276,3 @@ class SocketSecurity(IntEnum):
SSL = 1
STARTTLS = 2
PLAIN = 3
class VideochatType(IntEnum):
"""Video chat URL type."""
UNKNOWN = 0
BASICWEBRTC = 1
JITSI = 2

View File

@@ -93,6 +93,17 @@ class Message:
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
break
def resend(self) -> None:
"""Resend messages and make information available for newly added chat members.
Resending sends out the original message, however, recipients and webxdc-status may differ.
Clients that already have the original message can still ignore the resent message as
they have tracked the state by dedicated updates.
Some messages cannot be resent, eg. info-messages, drafts, already pending messages,
or messages that are not sent by SELF.
"""
self._rpc.resend_messages(self.account.id, [self.id])
@futuremethod
def send_webxdc_realtime_advertisement(self):
"""Send an advertisement to join the realtime channel."""
@@ -102,3 +113,15 @@ class Message:
def send_webxdc_realtime_data(self, data) -> None:
"""Send data to the realtime channel."""
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
def accept_incoming_call(self, accept_call_info):
"""Accepts an incoming call."""
self._rpc.accept_incoming_call(self.account.id, self.id, accept_call_info)
def end_call(self):
"""Ends incoming or outgoing call."""
self._rpc.end_call(self.account.id, self.id)
def get_call_info(self) -> AttrDict:
"""Return information about the call."""
return AttrDict(self._rpc.call_info(self.account.id, self.id))

View File

@@ -28,9 +28,7 @@ class ACFactory:
def get_unconfigured_account(self) -> Account:
"""Create a new unconfigured account."""
account = self.deltachat.add_account()
account.set_config("verified_one_on_one_chats", "1")
return account
return self.deltachat.add_account()
def get_unconfigured_bot(self) -> Bot:
"""Create a new unconfigured bot."""
@@ -45,10 +43,9 @@ class ACFactory:
@futuremethod
def new_configured_account(self):
"""Create a new configured account."""
addr, password = self.get_credentials()
account = self.get_unconfigured_account()
params = {"addr": addr, "password": password}
yield account.add_or_update_transport.future(params)
domain = os.getenv("CHATMAIL_DOMAIN")
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
assert account.is_configured()
return account
@@ -75,11 +72,11 @@ class ACFactory:
def resetup_account(self, ac: Account) -> Account:
"""Resetup account from scratch, losing the encryption key."""
ac.stop_io()
ac_clone = self.get_unconfigured_account()
for i in ["addr", "mail_pw"]:
ac_clone.set_config(i, ac.get_config(i))
transports = ac.list_transports()
ac.remove()
ac_clone.configure()
ac_clone = self.get_unconfigured_account()
for transport in transports:
ac_clone.add_or_update_transport(transport)
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:

View File

@@ -85,11 +85,11 @@ class DirectImap:
def get_all_messages(self) -> list[MailMessage]:
assert not self._idling
return list(self.conn.fetch())
return list(self.conn.fetch(mark_seen=False))
def get_unread_messages(self) -> list[str]:
assert not self._idling
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
return [msg.uid for msg in self.conn.fetch(AND(seen=False), mark_seen=False)]
def mark_all_read(self):
messages = self.get_unread_messages()
@@ -173,7 +173,6 @@ class DirectImap:
class IdleManager:
def __init__(self, direct_imap) -> None:
self.direct_imap = direct_imap
self.log = direct_imap.account.log
# fetch latest messages before starting idle so that it only
# returns messages that arrive anew
self.direct_imap.conn.fetch("1:*")
@@ -181,14 +180,11 @@ class IdleManager:
def check(self, timeout=None) -> list[bytes]:
"""(blocking) wait for next idle message from server."""
self.log("imap-direct: calling idle_check")
res = self.direct_imap.conn.idle.poll(timeout=timeout)
self.log(f"imap-direct: idle_check returned {res!r}")
return res
return self.direct_imap.conn.idle.poll(timeout=timeout)
def wait_for_new_message(self, timeout=None) -> bytes:
def wait_for_new_message(self) -> bytes:
while True:
for item in self.check(timeout=timeout):
for item in self.check():
if b"EXISTS" in item or b"RECENT" in item:
return item
@@ -196,10 +192,8 @@ class IdleManager:
"""Return first message with SEEN flag from a running idle-stream."""
while True:
for item in self.check(timeout=timeout):
if FETCH in item:
self.log(str(item))
if FLAGS in item and rb"\Seen" in item:
return int(item.split(b" ")[1])
if FETCH in item and FLAGS in item and rb"\Seen" in item:
return int(item.split(b" ")[1])
def done(self):
"""send idle-done to server if we are currently in idle mode."""

View File

@@ -0,0 +1,109 @@
from deltachat_rpc_client import EventType, Message
def test_calls(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
place_call_info = "offer"
accept_call_info = "answer"
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
assert incoming_call_event.place_call_info == place_call_info
assert not incoming_call_event.has_video # Cannot be parsed as SDP, so false by default
incoming_call_message = Message(bob, incoming_call_event.msg_id)
assert incoming_call_message.get_call_info().state.kind == "Alerting"
assert not incoming_call_message.get_call_info().has_video
incoming_call_message.accept_incoming_call(accept_call_info)
assert incoming_call_message.get_call_info().sdp_offer == place_call_info
assert incoming_call_message.get_call_info().state.kind == "Active"
outgoing_call_accepted_event = alice.wait_for_event(EventType.OUTGOING_CALL_ACCEPTED)
assert outgoing_call_accepted_event.accept_call_info == accept_call_info
assert outgoing_call_message.get_call_info().state.kind == "Active"
outgoing_call_message.end_call()
assert outgoing_call_message.get_call_info().state.kind == "Completed"
end_call_event = bob.wait_for_event(EventType.CALL_ENDED)
assert end_call_event.msg_id == outgoing_call_message.id
assert incoming_call_message.get_call_info().state.kind == "Completed"
def test_video_call(acfactory) -> None:
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
# with `s= ` replaced with `s=-`.
#
# `s=` cannot be empty according to RFC 3264,
# so it is more clear as `s=-`.
place_call_info = """v=0\r
o=alice 2890844526 2890844526 IN IP6 2001:db8::3\r
s=-\r
c=IN IP6 2001:db8::3\r
t=0 0\r
a=group:BUNDLE foo bar\r
\r
m=audio 10000 RTP/AVP 0 8 97\r
b=AS:200\r
a=mid:foo\r
a=rtcp-mux\r
a=rtpmap:0 PCMU/8000\r
a=rtpmap:8 PCMA/8000\r
a=rtpmap:97 iLBC/8000\r
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
\r
m=video 10002 RTP/AVP 31 32\r
b=AS:1000\r
a=mid:bar\r
a=rtcp-mux\r
a=rtpmap:31 H261/90000\r
a=rtpmap:32 MPV/90000\r
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
"""
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.place_outgoing_call(place_call_info)
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
assert incoming_call_event.place_call_info == place_call_info
assert incoming_call_event.has_video
incoming_call_message = Message(bob, incoming_call_event.msg_id)
assert incoming_call_message.get_call_info().has_video
def test_ice_servers(acfactory) -> None:
alice = acfactory.get_online_account()
ice_servers = alice.ice_servers()
assert len(ice_servers) == 1
def test_no_contact_request_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
alice_chat_bob.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.MSGS_CHANGED:
msg = bob.get_message_by_id(event.msg_id)
if msg.get_snapshot().text == "Hello!":
break

View File

@@ -169,6 +169,8 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
bob.create_chat(alice)
alice_chat_bob.send_text("hello")
msg = bob.wait_for_incoming_msg()

View File

@@ -0,0 +1,538 @@
import logging
import re
import time
import pytest
from imap_tools import AND, U
from deltachat_rpc_client import Contact, EventType, Message
def test_move_works(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.set_config("mvbox_move", "1")
ac2.bring_online()
chat = ac1.create_chat(ac2)
chat.send_text("message1")
# Message is moved to the movebox
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
# Message is downloaded
msg = ac2.wait_for_incoming_msg().get_snapshot()
assert msg.text == "message1"
def test_move_avoids_loop(acfactory, direct_imap):
"""Test that the message is only moved from INBOX to DeltaChat.
This is to avoid busy loop if moved message reappears in the Inbox
or some scanned folder later.
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.set_config("mvbox_move", "1")
ac2.set_config("delete_server_after", "0")
ac2.bring_online()
# Create INBOX.DeltaChat folder and make sure
# it is detected by full folder scan.
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("INBOX.DeltaChat")
ac2.stop_io()
ac2.start_io()
while True:
event = ac2.wait_for_event()
# Wait until the end of folder scan.
if event.kind == EventType.INFO and "Found folders:" in event.msg:
break
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
ac1_chat.send_text("Message 1")
# Message is moved to the DeltaChat folder and downloaded.
ac2_msg1 = ac2.wait_for_incoming_msg().get_snapshot()
assert ac2_msg1.text == "Message 1"
# Move the message to the INBOX.DeltaChat again.
# We assume that test server uses "." as the delimiter.
ac2_direct_imap.select_folder("DeltaChat")
ac2_direct_imap.conn.move(["*"], "INBOX.DeltaChat")
ac1_chat.send_text("Message 2")
ac2_msg2 = ac2.wait_for_incoming_msg().get_snapshot()
assert ac2_msg2.text == "Message 2"
# Stop and start I/O to trigger folder scan.
ac2.stop_io()
ac2.start_io()
while True:
event = ac2.wait_for_event()
# Wait until the end of folder scan.
if event.kind == EventType.INFO and "Found folders:" in event.msg:
break
# Check that Message 1 is still in the INBOX.DeltaChat folder
# and Message 2 is in the DeltaChat folder.
ac2_direct_imap.select_folder("INBOX")
assert len(ac2_direct_imap.get_all_messages()) == 0
ac2_direct_imap.select_folder("DeltaChat")
assert len(ac2_direct_imap.get_all_messages()) == 1
ac2_direct_imap.select_folder("INBOX.DeltaChat")
assert len(ac2_direct_imap.get_all_messages()) == 1
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.set_config("mvbox_move", "1")
assert ac2.is_configured()
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
logging.info("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
msg1.wait_until_delivered()
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")
logging.info("receiving messages by ac2")
ac2.start_io()
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_snapshot().text == msg1.get_snapshot().text
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_delete_deltachat_folder(acfactory, direct_imap):
"""Test that DeltaChat folder is recreated if user deletes it manually."""
ac1 = acfactory.new_configured_account()
ac1.set_config("mvbox_move", "1")
ac1.bring_online()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.conn.folder.delete("DeltaChat")
assert "DeltaChat" not in ac1_direct_imap.list_folders()
# Wait until new folder is created and UIDVALIDITY is updated.
while True:
event = ac1.wait_for_event()
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
break
ac2 = acfactory.get_online_account()
ac2.create_chat(ac1).send_text("hello")
msg = ac1.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
assert "DeltaChat" in ac1_direct_imap.list_folders()
def test_dont_show_emails(acfactory, direct_imap, log):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header, then ignore the email.
If the draft email is sent out and received later (i.e. it's in "Inbox"), it must be shown.
Also, test that unknown emails in the Spam folder are not shown."""
ac1 = acfactory.new_configured_account()
ac1.stop_io()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.org").create_chat()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.create_folder("Drafts")
ac1_direct_imap.create_folder("Spam")
ac1_direct_imap.create_folder("Junk")
# Learn UID validity for all folders.
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.start_io()
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
ac1.stop_io()
ac1_direct_imap.append(
"Drafts",
"""
From: ac1 <{}>
Subject: subj
To: alice@example.org
Message-ID: <aepiors@example.org>
Content-Type: text/plain; charset=utf-8
message in Drafts received later
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org, unkwnown.add@junk.org
Subject: subj
To: {}
Message-ID: <spam.message2@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Spam",
"""
From: delta<address: inbox@nhroy.com>
Subject: subj
To: {}
Message-ID: <spam.message99@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Spam",
"""
From: alice@example.org
Subject: subj
To: {}
Message-ID: <spam.message3@junk.org>
Content-Type: text/plain; charset=utf-8
Actually interesting message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Junk",
"""
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Junk
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.set_config("scan_all_folders_debounce_secs", "0")
log.section("All prepared, now let DC find the message")
ac1.start_io()
# Wait until each folder was scanned, this is necessary for this test to test what it should test:
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
fresh_msgs = list(ac1.get_fresh_messages())
msg = fresh_msgs[0].get_snapshot()
chat_msgs = msg.chat.get_messages()
assert len(chat_msgs) == 1
assert msg.text == "subj Actually interesting message in Spam"
assert not any("unknown.address" in c.get_full_snapshot().name for c in ac1.get_chatlist())
ac1_direct_imap.select_folder("Spam")
assert ac1_direct_imap.get_uid_by_message_id("spam.message@junk.org")
ac1.stop_io()
log.section("'Send out' the draft by moving it to Inbox, and wait for DC to display it this time")
ac1_direct_imap.select_folder("Drafts")
uid = ac1_direct_imap.get_uid_by_message_id("aepiors@example.org")
ac1_direct_imap.conn.move(uid, "Inbox")
ac1.start_io()
event = ac1.wait_for_event(EventType.MSGS_CHANGED)
msg2 = Message(ac1, event.msg_id).get_snapshot()
assert msg2.text == "subj message in Drafts received later"
assert len(msg.chat.get_messages()) == 2
def test_move_works_on_self_sent(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
# Enable movebox and wait until it is created.
ac1.set_config("mvbox_move", "1")
ac1.set_config("bcc_self", "1")
ac1.bring_online()
chat = ac1.create_chat(ac2)
chat.send_text("message1")
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
chat.send_text("message2")
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
chat.send_text("message3")
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
def test_moved_markseen(acfactory, direct_imap):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.set_config("mvbox_move", "1")
ac2.set_config("delete_server_after", "0")
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
ac2.bring_online()
ac2.stop_io()
ac2_direct_imap = direct_imap(ac2)
with ac2_direct_imap.idle() as idle2:
ac1.create_chat(ac2).send_text("Hello!")
idle2.wait_for_new_message()
# Emulate moving of the message to DeltaChat folder by Sieve rule.
ac2_direct_imap.conn.move(["*"], "DeltaChat")
ac2_direct_imap.select_folder("DeltaChat")
assert len(list(ac2_direct_imap.conn.fetch("*", mark_seen=False))) == 1
with ac2_direct_imap.idle() as idle2:
ac2.start_io()
ev = ac2.wait_for_event(EventType.MSGS_CHANGED)
msg = ac2.get_message_by_id(ev.msg_id)
assert msg.get_snapshot().text == "Messages are end-to-end encrypted."
ev = ac2.wait_for_event(EventType.INCOMING_MSG)
msg = ac2.get_message_by_id(ev.msg_id)
chat = ac2.get_chat_by_id(ev.chat_id)
# Accept the contact request.
chat.accept()
msg.mark_seen()
idle2.wait_for_seen()
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
@pytest.mark.parametrize("mvbox_move", [True, False])
def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
ac1, ac2 = acfactory.get_online_accounts(2)
for ac in ac1, ac2:
ac.set_config("delete_server_after", "0")
if mvbox_move:
ac.set_config("mvbox_move", "1")
ac.bring_online()
# Do not send BCC to self, we only want to test MDN on ac1.
ac1.set_config("bcc_self", "0")
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2.wait_for_incoming_msg()
msg.mark_seen()
if mvbox_move:
rex = re.compile("Marked messages [0-9]+ in folder DeltaChat as seen.")
else:
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
for ac in ac1, ac2:
while True:
event = ac.wait_for_event()
if event.kind == EventType.INFO and rex.search(event.msg):
break
folder = "mvbox" if mvbox_move else "inbox"
ac1_direct_imap = direct_imap(ac1)
ac2_direct_imap = direct_imap(ac2)
ac1_direct_imap.select_config_folder(folder)
ac2_direct_imap.select_config_folder(folder)
# Check that the mdn is marked as seen
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
# Check original message is marked as seen
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
def test_mvbox_and_trash(acfactory, direct_imap, log):
log.section("ac1: start with mvbox")
ac1 = acfactory.get_online_account()
ac1.set_config("mvbox_move", "1")
ac1.bring_online()
log.section("ac2: start without a mvbox")
ac2 = acfactory.get_online_account()
log.section("ac1: create trash")
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.create_folder("Trash")
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.stop_io()
ac1.start_io()
log.section("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
assert ac2.wait_for_incoming_msg().get_snapshot().text == "message1"
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
while ac1.get_config("configured_trash_folder") != "Trash":
ac1.wait_for_event(EventType.CONNECTIVITY_CHANGED)
@pytest.mark.parametrize(
("folder", "move", "expected_destination"),
[
(
"xyz",
False,
"xyz",
), # Test that emails aren't found in a random folder
(
"xyz",
True,
"xyz",
), # ...emails are found in a random folder and downloaded without moving
(
"Spam",
False,
"INBOX",
), # ...emails are moved from the spam folder to the Inbox
],
)
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
def test_scan_folders(acfactory, log, direct_imap, folder, move, expected_destination):
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
variant = folder + "-" + str(move) + "-" + expected_destination
log.section("Testing variant " + variant)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("delete_server_after", "0")
if move:
ac1.set_config("mvbox_move", "1")
ac1.bring_online()
ac1.stop_io()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.create_folder(folder)
# Wait until each folder was selected once and we are IDLEing:
ac1.start_io()
ac1.bring_online()
ac1.stop_io()
assert folder in ac1_direct_imap.list_folders()
log.section("Send a message from ac2 to ac1 and manually move it to `folder`")
ac1_direct_imap.select_config_folder("inbox")
with ac1_direct_imap.idle() as idle1:
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
idle1.wait_for_new_message()
ac1_direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
log.section("start_io() and see if DeltaChat finds the message (" + variant + ")")
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.start_io()
chat = ac1.create_chat(ac2)
n_msgs = 1 # "Messages are end-to-end encrypted."
if folder == "Spam":
msg = ac1.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
n_msgs += 1
else:
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
assert len(chat.get_messages()) == n_msgs
# The message has reached its destination.
ac1_direct_imap.select_folder(expected_destination)
assert len(ac1_direct_imap.get_all_messages()) == 1
if folder != expected_destination:
ac1_direct_imap.select_folder(folder)
assert len(ac1_direct_imap.get_all_messages()) == 0
def test_trash_multiple_messages(acfactory, direct_imap, log):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.stop_io()
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
# Trash wasn't configured initially, it can't be configured later, let's check this.
log.section("Creating trash folder")
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("Trash")
ac2.set_config("delete_server_after", "0")
ac2.set_config("sync_msgs", "0")
ac2.set_config("delete_to_trash", "1")
log.section("Check that Trash can be configured initially as well")
ac3 = ac2.clone()
ac3.bring_online()
assert ac3.get_config("configured_trash_folder")
ac3.stop_io()
ac2.start_io()
chat12 = acfactory.get_accepted_chat(ac1, ac2)
log.section("ac1: sending 3 messages")
texts = ["first", "second", "third"]
for text in texts:
chat12.send_text(text)
log.section("ac2: waiting for all messages on the other side")
to_delete = []
for text in texts:
msg = ac2.wait_for_incoming_msg().get_snapshot()
assert msg.text in texts
if text != "second":
to_delete.append(msg)
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
# check the configuration.
assert ac2.get_config("configured_trash_folder") == "Trash"
log.section("ac2: deleting all messages except second")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
log.section("ac2: test that only one message is left")
while 1:
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
ac2_direct_imap.select_config_folder("inbox")
nr_msgs = len(ac2_direct_imap.get_all_messages())
assert nr_msgs > 0
if nr_msgs == 1:
break

View File

@@ -84,7 +84,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
snapshot = ac2_webxdc_msg.get_snapshot()
assert snapshot.text == "play"
@@ -94,7 +94,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
log("waiting for incoming message on ac2")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "ping1"
log("sending ac2 -> ac1 realtime advertisement and additional message")
@@ -102,7 +102,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
log("waiting for incoming message on ac1")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "ping2"
log("sending realtime data ac1 -> ac2")
@@ -214,7 +214,9 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
ac2_webxdc_msg_snapshot = ac2_webxdc_msg.get_snapshot()
assert ac2_webxdc_msg_snapshot.text == "WebXDC"
ac2_webxdc_msg_snapshot.chat.accept()
ac1_ac2_chat.send_text("Hello!")
ac2_hello_msg = ac2.wait_for_incoming_msg()

View File

@@ -18,9 +18,7 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
alice1 = acfactory.get_online_account()
alice2 = acfactory.get_unconfigured_account()
alice2.set_config("addr", alice1.get_config("addr"))
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
alice2.configure()
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
alice2.bring_online()
setup_code = alice1.initiate_autocrypt_key_transfer()
@@ -37,9 +35,7 @@ def test_ac_setup_message_twice(acfactory):
alice1 = acfactory.get_online_account()
alice2 = acfactory.get_unconfigured_account()
alice2.set_config("addr", alice1.get_config("addr"))
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
alice2.configure()
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
alice2.bring_online()
# Send the first Autocrypt Setup Message and ignore it.

View File

@@ -3,6 +3,7 @@ import logging
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
from deltachat_rpc_client.const import ChatType
from deltachat_rpc_client.rpc import JsonRpcError
@@ -58,8 +59,7 @@ def test_qr_setup_contact_svg(acfactory) -> None:
assert "Alice" in svg
@pytest.mark.parametrize("protect", [True, False])
def test_qr_securejoin(acfactory, protect):
def test_qr_securejoin(acfactory):
alice, bob, fiona = acfactory.get_online_accounts(3)
# Setup second device for Alice
@@ -67,8 +67,7 @@ def test_qr_securejoin(acfactory, protect):
alice2 = alice.clone()
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group", protect=protect)
assert alice_chat.get_basic_snapshot().is_protected == protect
alice_chat = alice.create_group("Group")
logging.info("Bob joins the group")
qr_code = alice_chat.get_qr_code()
@@ -87,9 +86,8 @@ def test_qr_securejoin(acfactory, protect):
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
@@ -112,6 +110,143 @@ def test_qr_securejoin(acfactory, protect):
fiona.wait_for_securejoin_joiner_success()
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
alice, bob, fiona = acfactory.get_online_accounts(3)
alice2 = alice.clone()
bob2 = bob.clone()
if all_devices_online:
alice2.start_io()
bob2.start_io()
logging.info("===================== Alice creates a broadcast =====================")
alice_chat = alice.create_broadcast("Broadcast channel!")
snapshot = alice_chat.get_basic_snapshot()
assert not snapshot.is_unpromoted # Broadcast channels are never unpromoted
logging.info("===================== Bob joins the broadcast =====================")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
alice_chat.send_text("Hello everyone!")
def get_broadcast(ac):
chat = ac.get_chatlist(query="Broadcast channel!")[0]
assert chat.get_basic_snapshot().name == "Broadcast channel!"
return chat
def wait_for_broadcast_messages(ac):
chat = get_broadcast(ac)
snapshot = ac.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You joined the channel."
assert snapshot.chat_id == chat.id
snapshot = ac.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello everyone!"
assert snapshot.chat_id == chat.id
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
# Check that the chat partner is verified.
contact_snapshot = contact.get_snapshot()
assert contact_snapshot.is_verified
chat = get_broadcast(ac)
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs[0].get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
member_added_msg = chat_msgs[1].get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
else:
assert member_added_msg.text == "You joined the channel."
assert member_added_msg.is_info
hello_msg = chat_msgs[2].get_snapshot()
assert hello_msg.text == "Hello everyone!"
assert not hello_msg.is_info
assert hello_msg.show_padlock
assert hello_msg.error is None
assert len(chat_msgs) == 3
chat_snapshot = chat.get_full_snapshot()
assert chat_snapshot.is_encrypted
assert chat_snapshot.name == "Broadcast channel!"
if inviter_side:
assert chat_snapshot.chat_type == ChatType.OUT_BROADCAST
else:
assert chat_snapshot.chat_type == ChatType.IN_BROADCAST
assert chat_snapshot.can_send == inviter_side
chat_contacts = chat_snapshot.contact_ids
assert contact.id in chat_contacts
if inviter_side:
assert len(chat_contacts) == 1
else:
assert len(chat_contacts) == 2
assert SpecialContactId.SELF in chat_contacts
assert chat_snapshot.self_in_group
wait_for_broadcast_messages(bob)
check_account(alice, alice.create_contact(bob), inviter_side=True)
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
logging.info("===================== Test Alice's second device =====================")
# Start second Alice device, if it wasn't started already.
alice2.start_io()
while True:
msg_id = alice2.wait_for_msgs_changed_event().msg_id
if msg_id:
snapshot = alice2.get_message_by_id(msg_id).get_snapshot()
if snapshot.text == "Hello everyone!":
break
check_account(alice2, alice2.create_contact(bob), inviter_side=True)
logging.info("===================== Test Bob's second device =====================")
# Start second Bob device, if it wasn't started already.
bob2.start_io()
bob2.wait_for_securejoin_joiner_success()
wait_for_broadcast_messages(bob2)
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
# The QR code token is synced, so alice2 must be able to handle join requests.
logging.info("===================== Fiona joins the group via alice2 =====================")
alice.stop_io()
fiona.secure_join(qr_code)
alice2.wait_for_securejoin_inviter_success()
fiona.wait_for_securejoin_joiner_success()
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You joined the channel."
get_broadcast(alice2).get_messages()[2].resend()
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello everyone!"
check_account(fiona, fiona.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
# For Bob, the channel must not have changed:
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acfactory.get_online_accounts(2)
@@ -120,13 +255,13 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello!"
bob_chat_alice = snapshot.chat
assert bob_chat_alice.get_basic_snapshot().is_contact_request
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
alice_chat = alice.create_group("Group")
logging.info("Bob joins the group")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
@@ -150,8 +285,8 @@ def test_qr_readreceipt(acfactory) -> None:
for joiner in [bob, charlie]:
joiner.wait_for_securejoin_joiner_success()
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
logging.info("Alice creates a group")
group = alice.create_group("Group")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
@@ -164,8 +299,7 @@ def test_qr_readreceipt(acfactory) -> None:
logging.info("Bob and Charlie receive a group")
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
bob_message = bob.get_message_by_id(bob_msg_id)
bob_message = bob.wait_for_incoming_msg()
bob_snapshot = bob_message.get_snapshot()
assert bob_snapshot.text == "Hello"
@@ -176,8 +310,7 @@ def test_qr_readreceipt(acfactory) -> None:
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
charlie_message = charlie.get_message_by_id(charlie_msg_id)
charlie_message = charlie.wait_for_incoming_msg()
charlie_snapshot = charlie_message.get_snapshot()
assert charlie_snapshot.text == "Hi from Bob!"
@@ -216,11 +349,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac1 creates a group")
chat = ac1.create_group("Group")
logging.info("ac2 joins verified group")
logging.info("ac2 joins the group")
qr_code = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -253,7 +385,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac3_contact_ac2 = ac3.create_contact(ac2)
ac3_chat.remove_contact(ac3_contact_ac2_old)
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert "removed" in snapshot.text
ac3_chat.add_contact(ac3_contact_ac2)
@@ -266,25 +398,26 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac2 got event message: %s", snapshot.text)
assert "added" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert "added" in snapshot.text
chat = Chat(ac2, chat_id)
chat.send_text("Works again!")
msg_id = ac3.wait_for_incoming_msg_event().msg_id
message = ac3.get_message_by_id(msg_id)
message = ac3.wait_for_incoming_msg()
snapshot = message.get_snapshot()
assert snapshot.text == "Works again!"
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Works again!"
ac1_contact_ac2 = ac1.create_contact(ac2)
ac1_contact_ac3 = ac1.create_contact(ac3)
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
assert ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
@@ -302,8 +435,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
logging.info("ac1: create a group that ac2 fully joins")
ch1 = ac1.create_group("Group")
qr_code = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
@@ -311,9 +444,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
if snapshot.text == "ac1 says hello":
assert snapshot.chat.get_basic_snapshot().is_protected
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
@@ -327,15 +459,14 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
assert ac2.create_contact(ac3).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
vg = ac3.create_group("ac3-created")
vg.add_contact(ac3.create_contact(ac2))
# ensure ac2 receives message in VG
vg.send_text("hello")
while 1:
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
msg = ac2.wait_for_incoming_msg().get_snapshot()
if msg.text == "hello":
assert msg.chat.get_basic_snapshot().is_protected
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
@@ -359,7 +490,7 @@ def test_qr_new_group_unblocked(acfactory):
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining", protect=True)
ac1_chat = ac1.create_group("Group for joining")
qr_code = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
@@ -371,7 +502,7 @@ def test_qr_new_group_unblocked(acfactory):
ac2.wait_for_incoming_msg_event()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
ac2_msg = ac2.wait_for_incoming_msg().get_snapshot()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
@@ -384,8 +515,7 @@ def test_aeap_flow_verified(acfactory):
addr, password = acfactory.get_credentials()
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
chat = ac1.create_group("hello")
qr_code = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
@@ -397,7 +527,7 @@ def test_aeap_flow_verified(acfactory):
logging.info("receiving first message")
ac2.wait_for_incoming_msg_event() # member added message
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
msg_in_1 = ac2.wait_for_incoming_msg().get_snapshot()
assert msg_in_1.text == msg_out.text
logging.info("changing email account")
@@ -411,7 +541,7 @@ def test_aeap_flow_verified(acfactory):
msg_out = chat.send_text("changed address").get_snapshot()
logging.info("receiving second message")
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
msg_in_2 = ac2.wait_for_incoming_msg()
msg_in_2_snapshot = msg_in_2.get_snapshot()
assert msg_in_2_snapshot.text == msg_out.text
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
@@ -439,33 +569,35 @@ def test_gossip_verification(acfactory) -> None:
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
assert not bob_group_chat.get_basic_snapshot().is_protected
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Autocrypt group")
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = carol.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Autocrypt group"
assert snapshot.show_padlock
# Autocrypt group does not propagate verification.
# Group propagates verification using Autocrypt-Gossip header.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not carol_contact_alice_snapshot.is_verified
logging.info("Bob creates a Securejoin group")
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
assert bob_group_chat.get_basic_snapshot().is_protected
bob_group_chat = bob.create_group("Securejoin Group")
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Securejoin group")
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = carol.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Securejoin group"
assert snapshot.show_padlock
# Securejoin propagates verification.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
assert carol_contact_alice_snapshot.is_verified
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not carol_contact_alice_snapshot.is_verified
def test_securejoin_after_contact_resetup(acfactory) -> None:
@@ -477,7 +609,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac3 creates protected group with ac1.
ac3_chat = ac3.create_group("Verified group", protect=True)
ac3_chat = ac3.create_group("Group")
# ac1 joins ac3 group.
ac3_qr_code = ac3_chat.get_qr_code()
@@ -485,7 +617,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
ac1.wait_for_securejoin_joiner_success()
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code()
@@ -522,10 +654,9 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# Wait for member added.
logging.info("ac2 waits for member added message")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
assert snapshot.is_info
ac2_chat = snapshot.chat
assert ac2_chat.get_basic_snapshot().is_protected
assert len(ac2_chat.get_contacts()) == 3
# ac1 is still "not verified" for ac2 due to inconsistent state.
@@ -535,9 +666,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group")
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
@@ -546,9 +676,8 @@ def test_withdraw_securejoin_qr(acfactory):
alice.clear_all_events()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()

View File

@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
from deltachat_rpc_client.rpc import JsonRpcError
@@ -171,7 +171,10 @@ def test_account(acfactory) -> None:
assert alice.get_size()
assert alice.is_configured()
assert not alice.get_avatar()
assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
# get_contact_by_addr() can lookup a key contact by address:
bob_contact = alice.get_contact_by_addr(bob_addr).get_snapshot()
assert bob_contact.display_name == "Bob"
assert bob_contact.is_key_contact
assert alice.get_contacts()
assert alice.get_contacts(snapshot=True)
assert alice.self_contact
@@ -249,6 +252,7 @@ def test_chat(acfactory) -> None:
bob_chat_alice.get_encryption_info()
group = alice.create_group("test group")
to_resend = group.send_text("will be resent")
group.add_contact(alice_contact_bob)
group.get_qr_code()
@@ -260,6 +264,7 @@ def test_chat(acfactory) -> None:
msg = group.send_message(text="hi")
assert (msg.get_snapshot()).text == "hi"
group.resend_messages([to_resend])
group.forward_messages([msg])
group.set_draft(text="test draft")
@@ -326,6 +331,34 @@ def test_message(acfactory) -> None:
assert reactions == snapshot.reactions
def test_receive_imf_failure(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
bob.set_config("fail_on_receiving_full_msg", "1")
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_event(EventType.MSGS_CHANGED)
assert event.chat_id == bob.get_device_chat().id
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert (
snapshot.text == "❌ Failed to receive a message:"
" Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
)
# The failed message doesn't break the IMAP loop.
bob.set_config("fail_on_receiving_full_msg", "0")
alice_chat_bob.send_text("Hello again!")
message = bob.wait_for_incoming_msg()
snapshot = message.get_snapshot()
assert snapshot.text == "Hello again!"
assert snapshot.download_state == DownloadState.DONE
assert snapshot.error is None
def test_selfavatar_sync(acfactory, data, log) -> None:
alice = acfactory.get_online_account()
@@ -388,10 +421,7 @@ def test_is_bot(acfactory) -> None:
alice.set_config("bot", "1")
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.chat_id == event.chat_id
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello!"
assert snapshot.is_bot
@@ -449,22 +479,21 @@ def test_wait_next_messages(acfactory) -> None:
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# Bot starts waiting for messages.
next_messages_task = executor.submit(bot.wait_next_messages)
# Bot starts waiting for messages.
next_messages_task = bot.wait_next_messages.future()
alice_contact_bot = alice.create_contact(bot, "Bot")
alice_chat_bot = alice_contact_bot.create_chat()
alice_chat_bot.send_text("Hello!")
alice_contact_bot = alice.create_contact(bot, "Bot")
alice_chat_bot = alice_contact_bot.create_chat()
alice_chat_bot.send_text("Hello!")
next_messages = next_messages_task.result()
next_messages = next_messages_task()
if len(next_messages) == E2EE_INFO_MSGS:
next_messages += bot.wait_next_messages()
if len(next_messages) == E2EE_INFO_MSGS:
next_messages += bot.wait_next_messages()
assert len(next_messages) == 1 + E2EE_INFO_MSGS
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
assert snapshot.text == "Hello!"
assert len(next_messages) == 1 + E2EE_INFO_MSGS
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
assert snapshot.text == "Hello!"
def test_import_export_backup(acfactory, tmp_path) -> None:
@@ -484,7 +513,7 @@ def test_import_export_keys(acfactory, tmp_path) -> None:
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Bob!"
# Alice resetups account, but keeps the key.
@@ -496,7 +525,7 @@ def test_import_export_keys(acfactory, tmp_path) -> None:
snapshot.chat.accept()
snapshot.chat.send_text("Hello Alice!")
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = alice.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Alice!"
assert snapshot.show_padlock
@@ -519,8 +548,11 @@ def test_provider_info(rpc) -> None:
assert provider_info is None
# Test MX record resolution.
# This previously resulted in Gmail provider
# because MX record pointed to google.com domain,
# but MX record resolution has been removed.
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info["id"] == "gmail"
assert provider_info is None
# Disable MX record resolution.
rpc.set_config(account_id, "proxy_enabled", "1")
@@ -538,18 +570,13 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
# Alice sends a message to Bob.
alice_chat_bob.send_text("Hello Bob!")
event = bob.wait_for_incoming_msg_event()
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
# Bob sends a message to Alice.
bob_chat_alice = snapshot.chat
bob_chat_alice.accept()
bob_chat_alice.send_text("Hello Alice!")
event = alice.wait_for_incoming_msg_event()
msg_id = event.msg_id
message = alice.get_message_by_id(msg_id)
message = alice.wait_for_incoming_msg()
snapshot = message.get_snapshot()
assert snapshot.show_padlock
@@ -559,10 +586,7 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
# Bob sends a message to Alice, it should also be encrypted.
bob_chat_alice.send_text("Hi Alice!")
event = alice.wait_for_incoming_msg_event()
msg_id = event.msg_id
message = alice.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot = alice.wait_for_incoming_msg().get_snapshot()
assert snapshot.show_padlock
@@ -620,50 +644,6 @@ def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.set_config("mvbox_move", "1")
assert ac2.is_configured()
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
logging.info("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
msg1.wait_until_delivered()
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")
logging.info("receiving messages by ac2")
ac2.start_io()
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_snapshot().text == msg1.get_snapshot().text
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
@pytest.mark.parametrize("n_accounts", [3, 2])
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
download_limit = 300000
@@ -675,19 +655,17 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
for account in others:
chat = account.create_chat(alice)
chat.send_text("Hello Alice!")
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
assert alice.wait_for_incoming_msg().get_snapshot().text == "Hello Alice!"
contact = alice.create_contact(account)
alice_group.add_contact(contact)
if n_accounts == 2:
bob_chat_alice = bob.create_chat(alice)
bob_chat_alice = bob.create_chat(alice)
bob.set_config("download_limit", str(download_limit))
alice_group.send_text("hi")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "hi"
bob_group = snapshot.chat
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
@@ -695,17 +673,9 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
for i in range(10):
logging.info("Sending message %s", i)
alice_group.send_file(str(path))
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
if n_accounts > 2:
assert snapshot.chat == bob_group
else:
# Group contains only Alice and Bob,
# so partially downloaded messages are
# hard to distinguish from private replies to group messages.
#
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
assert snapshot.chat == bob_chat_alice
assert snapshot.chat == bob_chat_alice
def test_markseen_contact_request(acfactory):
@@ -722,8 +692,8 @@ def test_markseen_contact_request(acfactory):
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
message = bob.wait_for_incoming_msg()
message2 = bob2.wait_for_incoming_msg()
assert message2.get_snapshot().state == MessageState.IN_FRESH
message.mark_seen()
@@ -745,7 +715,7 @@ def test_read_receipt(acfactory):
msg = bob.wait_for_incoming_msg()
msg.mark_seen()
read_msg = alice.get_message_by_id(alice.wait_for_event(EventType.MSG_READ).msg_id)
read_msg = alice.wait_for_msg(EventType.MSG_READ)
read_receipts = read_msg.get_read_receipts()
assert len(read_receipts) == 1
assert read_receipts[0].contact_id == alice_contact_bob.id
@@ -762,7 +732,7 @@ def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
# Certificate checks should be configured (not None)
assert "cert_automatic" in alice.get_info().used_account_settings
assert "cert_strict" in alice.get_info().used_account_settings
# "cert_old_automatic" is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
@@ -834,10 +804,12 @@ def test_rename_group(acfactory):
bob_msg = bob.wait_for_incoming_msg()
bob_chat = bob_msg.get_snapshot().chat
assert bob_chat.get_basic_snapshot().name == "Test group"
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
for name in ["Baz", "Foo bar", "Xyzzy"]:
alice_group.set_name(name)
bob.wait_for_incoming_msg_event()
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
assert bob_chat.get_basic_snapshot().name == name
@@ -849,58 +821,137 @@ def test_get_all_accounts_deadlock(rpc):
all_accounts()
def test_delete_deltachat_folder(acfactory, direct_imap):
"""Test that DeltaChat folder is recreated if user deletes it manually."""
ac1 = acfactory.new_configured_account()
ac1.set_config("mvbox_move", "1")
ac1.bring_online()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.conn.folder.delete("DeltaChat")
assert "DeltaChat" not in ac1_direct_imap.list_folders()
# Wait until new folder is created and UIDVALIDITY is updated.
while True:
event = ac1.wait_for_event()
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
break
ac2 = acfactory.get_online_account()
ac2.create_chat(ac1).send_text("hello")
msg = ac1.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
assert "DeltaChat" in ac1_direct_imap.list_folders()
def test_broadcast(acfactory):
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_leave_broadcast(acfactory, all_devices_online):
alice, bob = acfactory.get_online_accounts(2)
alice_chat = alice.create_broadcast("My great channel")
snapshot = alice_chat.get_basic_snapshot()
assert snapshot.name == "My great channel"
assert snapshot.is_unpromoted
assert snapshot.is_encrypted
assert snapshot.chat_type == ChatType.OUT_BROADCAST
bob2 = bob.clone()
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat.add_contact(alice_contact_bob)
if all_devices_online:
bob2.start_io()
alice_msg = alice_chat.send_message(text="hello").get_snapshot()
assert alice_msg.text == "hello"
assert alice_msg.show_padlock
logging.info("===================== Alice creates a broadcast =====================")
alice_chat = alice.create_broadcast("Broadcast channel!")
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
assert bob_msg.text == "hello"
assert bob_msg.show_padlock
assert bob_msg.error is None
logging.info("===================== Bob joins the broadcast =====================")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
bob_chat_snapshot = bob_chat.get_basic_snapshot()
assert bob_chat_snapshot.name == "My great channel"
assert not bob_chat_snapshot.is_unpromoted
assert bob_chat_snapshot.is_encrypted
assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
assert bob_chat_snapshot.is_contact_request
alice_bob_contact = alice.create_contact(bob)
alice_contacts = alice_chat.get_contacts()
assert len(alice_contacts) == 1 # 1 recipient
assert alice_contacts[0].id == alice_bob_contact.id
assert not bob_chat.can_send()
member_added_msg = bob.wait_for_incoming_msg()
assert member_added_msg.get_snapshot().text == "You joined the channel."
def get_broadcast(ac):
chat = ac.get_chatlist(query="Broadcast channel!")[0]
assert chat.get_basic_snapshot().name == "Broadcast channel!"
return chat
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
chat = get_broadcast(ac)
contact_snapshot = contact.get_snapshot()
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs.pop(0).get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
member_added_msg = chat_msgs.pop(0).get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
else:
assert member_added_msg.text == "You joined the channel."
assert member_added_msg.is_info
if not inviter_side:
leave_msg = chat_msgs.pop(0).get_snapshot()
assert leave_msg.text == "You left the channel."
assert len(chat_msgs) == 0
chat_snapshot = chat.get_full_snapshot()
# On Alice's side, SELF is not in the list of contact ids
# because OutBroadcast chats never contain SELF in the list.
# On Bob's side, SELF is not in the list because he left.
if inviter_side:
assert len(chat_snapshot.contact_ids) == 0
else:
assert chat_snapshot.contact_ids == [contact.id]
logging.info("===================== Bob leaves the broadcast =====================")
bob_chat = get_broadcast(bob)
assert bob_chat.get_full_snapshot().self_in_group
assert len(bob_chat.get_contacts()) == 2 # Alice and Bob
bob_chat.leave()
assert not bob_chat.get_full_snapshot().self_in_group
# After Bob left, only Alice will be left in Bob's memberlist
assert len(bob_chat.get_contacts()) == 1
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
logging.info("===================== Test Alice's device =====================")
while len(alice_chat.get_contacts()) != 0: # After Bob left, there will be 0 recipients
alice.wait_for_event(EventType.CHAT_MODIFIED)
check_account(alice, alice.create_contact(bob), inviter_side=True)
logging.info("===================== Test Bob's second device =====================")
# Start second Bob device, if it wasn't started already.
bob2.start_io()
member_added_msg = bob2.wait_for_incoming_msg()
assert member_added_msg.get_snapshot().text == "You joined the channel."
bob2_chat = get_broadcast(bob2)
# After Bob left, only Alice will be left in Bob's memberlist
while len(bob2_chat.get_contacts()) != 1:
bob2.wait_for_event(EventType.CHAT_MODIFIED)
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
def test_immediate_autodelete(acfactory, direct_imap, log):
ac1, ac2 = acfactory.get_online_accounts(2)
# "1" means delete immediately, while "0" means do not delete
ac2.set_config("delete_server_after", "1")
log.section("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
ac2.create_chat(ac1)
log.section("ac1: send message to ac2")
sent_msg = chat1.send_text("hello")
msg = ac2.wait_for_incoming_msg()
assert msg.get_snapshot().text == "hello"
log.section("ac2: wait for close/expunge on autodelete")
ac2.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
while True:
event = ac2.wait_for_event()
if event.kind == EventType.INFO and "Close/expunge succeeded." in event.msg:
break
log.section("ac2: check that message was autodeleted on server")
ac2_direct_imap = direct_imap(ac2)
assert len(ac2_direct_imap.get_all_messages()) == 0
log.section("ac2: Mark deleted message as seen and check that read receipt arrives")
msg.mark_seen()
ev = ac1.wait_for_event(EventType.MSG_READ)
assert ev.chat_id == chat1.id
assert ev.msg_id == sent_msg.id

View File

@@ -1,8 +1,12 @@
def test_vcard(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice, bob, fiona = acfactory.get_online_accounts(3)
bob.create_chat(alice)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
alice_contact_charlie_snapshot = alice_contact_charlie.get_snapshot()
alice_contact_fiona = alice.create_contact(fiona, "Fiona")
alice_contact_fiona_snapshot = alice_contact_fiona.get_snapshot()
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_contact(alice_contact_charlie)
@@ -12,3 +16,12 @@ def test_vcard(acfactory) -> None:
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.addr == "charlie@example.org"
assert snapshot.vcard_contact.color == alice_contact_charlie_snapshot.color
alice_chat_bob.send_contact(alice_contact_fiona)
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.key
assert snapshot.vcard_contact.color == alice_contact_fiona_snapshot.color

View File

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

View File

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

View File

@@ -41,22 +41,22 @@ async fn main_impl() -> Result<()> {
if let Some(first_arg) = args.next() {
if first_arg.to_str() == Some("--version") {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
return Err(anyhow!("Unrecognized argument {arg:?}"));
}
eprintln!("{}", &*DC_VERSION_STR);
return Ok(());
} else if first_arg.to_str() == Some("--openrpc") {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
return Err(anyhow!("Unrecognized argument {arg:?}"));
}
println!("{}", CommandApi::openrpc_specification()?);
return Ok(());
} else {
return Err(anyhow!("Unrecognized option {:?}", first_arg));
return Err(anyhow!("Unrecognized option {first_arg:?}"));
}
}
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
return Err(anyhow!("Unrecognized argument {arg:?}"));
}
// Install signal handlers early so that the shutdown is graceful starting from here.

View File

@@ -20,6 +20,11 @@ impl SystemTimeTools {
pub fn shift(duration: Duration) {
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
}
/// Simulates the system clock being rewound by `duration`.
pub fn shift_back(duration: Duration) {
*SYSTEM_TIME_SHIFT.write().unwrap() -= duration;
}
}
#[cfg(test)]

View File

@@ -36,10 +36,6 @@ skip = [
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "redox_syscall", version = "0.4.1" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rustix", version = "0.38.44" },
{ name = "serdect", version = "0.2.0" },
{ name = "spin", version = "0.9.8" },
@@ -48,6 +44,7 @@ skip = [
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },

View File

@@ -98,9 +98,6 @@
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
};
@@ -240,6 +237,9 @@
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
@@ -483,12 +483,6 @@
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
@@ -587,6 +581,7 @@
(python3.withPackages (pypkgs: with pypkgs; [
tox
]))
nodejs
];
};
}

View File

@@ -6,7 +6,7 @@ edition = "2021"
license = "MPL-2.0"
[dev-dependencies]
bolero = "0.13.3"
bolero = "0.13.4"
[dependencies]
mailparse = { workspace = true }

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.4.0"
version = "2.26.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"

View File

@@ -330,7 +330,21 @@ class Account:
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
"""get a contact for the email address or None if it's blocked or doesn't exist."""
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
@@ -390,18 +404,16 @@ class Account:
self,
name: str,
contacts: Optional[List[Contact]] = None,
verified: bool = False,
) -> Chat:
"""create a new group chat object.
Chats are unpromoted until the first message is sent.
:param contacts: list of contacts to add
:param verified: if true only verified contacts can be added.
:returns: a :class:`deltachat.chat.Chat` object.
"""
bytes_name = name.encode("utf8")
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
chat_id = lib.dc_create_group_chat(self._dc_context, 0, bytes_name)
chat = Chat(self, chat_id)
if contacts is not None:
for contact in contacts:

View File

@@ -142,13 +142,6 @@ class Chat:
"""
return bool(lib.dc_chat_can_send(self._dc_chat))
def is_protected(self) -> bool:
"""return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return bool(lib.dc_chat_is_protected(self._dc_chat))
def get_name(self) -> Optional[str]:
"""return name of this chat.

View File

@@ -8,7 +8,6 @@ from typing import Optional, Union
from . import const, props
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
from .reactions import Reactions
class Message:
@@ -164,17 +163,6 @@ class Message:
),
)
def send_reaction(self, reaction: str):
"""Send a reaction to message and return the resulting Message instance."""
msg_id = lib.dc_send_reaction(self.account._dc_context, self.id, as_dc_charpointer(reaction))
if msg_id == 0:
raise ValueError("reaction could not be send")
return Message.from_db(self.account, msg_id)
def get_reactions(self) -> Reactions:
"""Get :class:`deltachat.reactions.Reactions` to the message."""
return Reactions.from_msg(self)
def is_system_message(self):
"""return True if this message is a system/info message."""
return bool(lib.dc_msg_is_info(self._dc_msg))
@@ -447,10 +435,6 @@ class Message:
"""return True if it's a video message."""
return self._view_type == const.DC_MSG_VIDEO
def is_videochat_invitation(self):
"""return True if it's a videochat invitation message."""
return self._view_type == const.DC_MSG_VIDEOCHAT_INVITATION
def is_webxdc(self):
"""return True if it's a Webxdc message."""
return self._view_type == const.DC_MSG_WEBXDC
@@ -491,7 +475,6 @@ _view_type_mapping = {
"video": const.DC_MSG_VIDEO,
"file": const.DC_MSG_FILE,
"sticker": const.DC_MSG_STICKER,
"videochat": const.DC_MSG_VIDEOCHAT_INVITATION,
"webxdc": const.DC_MSG_WEBXDC,
}

View File

@@ -1,43 +0,0 @@
"""The Reactions object."""
from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array
class Reactions:
"""Reactions object.
You obtain instances of it through :class:`deltachat.message.Message`.
"""
def __init__(self, account, dc_reactions) -> None:
assert isinstance(account._dc_context, ffi.CData)
assert isinstance(dc_reactions, ffi.CData)
assert dc_reactions != ffi.NULL
self.account = account
self._dc_reactions = dc_reactions
def __repr__(self) -> str:
return f"<Reactions dc_reactions={self._dc_reactions}>"
@classmethod
def from_msg(cls, msg):
assert msg.id > 0
return cls(
msg.account,
ffi.gc(lib.dc_get_msg_reactions(msg.account._dc_context, msg.id), lib.dc_reactions_unref),
)
def get_contacts(self) -> list:
"""Get list of contacts reacted to the message.
:returns: list of :class:`deltachat.contact.Contact` objects for this reaction.
"""
from .contact import Contact
dc_array = ffi.gc(lib.dc_reactions_get_contacts(self._dc_reactions), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self.account, x)))
def get_by_contact(self, contact) -> str:
"""Get a string containing space-separated reactions of a single :class:`deltachat.contact.Contact`."""
return from_dc_charpointer(lib.dc_reactions_get_by_contact_id(self._dc_reactions, contact.id))

View File

@@ -523,7 +523,6 @@ class ACFactory:
assert "addr" in configdict and "mail_pw" in configdict, configdict
configdict.setdefault("bcc_self", False)
configdict.setdefault("mvbox_move", False)
configdict.setdefault("sentbox_watch", False)
configdict.setdefault("sync_msgs", False)
configdict.setdefault("delete_server_after", 0)
ac.update_config(configdict)
@@ -604,20 +603,6 @@ class ACFactory:
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def get_protected_chat(self, ac1: Account, ac2: Account):
chat = ac1.create_group_chat("Protected Group", verified=True)
qr = chat.get_join_qr()
ac2.qr_join_chat(qr)
ac2._evtracker.wait_securejoin_joiner_progress(1000)
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
assert msg.text == "Messages are end-to-end encrypted."
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text
return chat
def introduce_each_other(self, accounts, sending=True):
to_wait = []
for i, acc in enumerate(accounts):

View File

@@ -116,10 +116,8 @@ class TestGroupStressTests:
def test_qr_verified_group_and_chatting(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1_addr = ac1.get_self_contact().addr
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
chat1 = ac1.create_group_chat("hello")
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -142,7 +140,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: read message and check that it's a verified chat")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.chat.is_protected()
assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
@@ -173,8 +170,12 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
ac2_ac3_contact = ac2.get_contacts()[1]
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
for ac2_contact in chat2.get_contacts():
if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF:
continue
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert ac2.get_self_contact().get_verifier(ac2_contact) is None
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
@@ -266,8 +267,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
ac1_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -321,8 +321,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
ac1.set_avatar(avatar_path)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
ac2.qr_join_chat(qr)
@@ -336,7 +335,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert msg_in.is_system_message()
assert contact.addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
@@ -376,8 +374,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
chat1 = ac1.create_group_chat("hello")
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -402,29 +399,20 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
assert not ac2_offl_ac1_contact.is_verified()
chat2_offl = msg_in.chat
assert not chat2_offl.is_protected()
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
lp.sec("ac2_offl: receiving message")
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert msg_in.is_system_message()
assert msg_in.text == "Messages are end-to-end encrypted."
# We need to consume one event that has data2=0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev.data2 == 0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert not msg_in.is_system_message()
assert msg_in.text == "hi2"
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert msg_in.chat.is_protected()
assert ac2_offl_ac1_contact.is_verified()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not ac2_offl_ac1_contact.is_verified()
def test_deleted_msgs_dont_reappear(acfactory):

View File

@@ -5,7 +5,7 @@ import base64
from datetime import datetime, timezone
import pytest
from imap_tools import AND, U
from imap_tools import AND
import deltachat as dc
from deltachat import account_hookimpl, Message
@@ -160,32 +160,6 @@ def test_html_message(acfactory, lp):
assert html_text in msg2.html
def test_videochat_invitation_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
text = "You are invited to a video chat, click https://meet.jit.si/WxEGad0gGzX to join."
lp.sec("ac1: prepare and send text message to ac2")
msg1 = chat.send_text("message0")
assert not msg1.is_videochat_invitation()
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message0"
assert not msg2.is_videochat_invitation()
lp.sec("ac1: prepare and send videochat invitation to ac2")
msg1 = Message.new_empty(ac1, "videochat")
msg1.set_text(text)
msg1 = chat.send_msg(msg1)
assert msg1.is_videochat_invitation()
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == text
assert msg2.is_videochat_invitation()
def test_webxdc_message(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -295,94 +269,6 @@ def test_enable_mvbox_move(acfactory, lp):
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
lp.sec("ac2: start without mvbox/sentbox threads")
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
lp.sec("ac2 and ac1: waiting for configuration")
acfactory.bring_accounts_online()
lp.sec("ac1: create and configure sentbox")
ac1.direct_imap.create_folder("Sent")
ac1.set_config("sentbox_watch", "1")
lp.sec("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
while ac1.get_config("configured_sentbox_folder") != "Sent":
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def test_move_works(acfactory):
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1")
# Message is moved to the movebox
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
# Message is downloaded
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
def test_move_avoids_loop(acfactory):
"""Test that the message is only moved once.
This is to avoid busy loop if moved message reappears in the Inbox
or some scanned folder later.
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
"""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
ac1_chat.send_text("Message 1")
# Message is moved to the DeltaChat folder and downloaded.
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg1.text == "Message 1"
# Move the message to the INBOX again.
ac2.direct_imap.select_folder("DeltaChat")
ac2.direct_imap.conn.move(["*"], "INBOX")
ac1_chat.send_text("Message 2")
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg2.text == "Message 2"
# Check that Message 1 is still in the INBOX folder
# and Message 2 is in the DeltaChat folder.
ac2.direct_imap.select_folder("INBOX")
assert len(ac2.direct_imap.get_all_messages()) == 1
ac2.direct_imap.select_folder("DeltaChat")
assert len(ac2.direct_imap.get_all_messages()) == 1
def test_move_works_on_self_sent(acfactory):
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
ac2 = acfactory.new_online_configuring_account()
acfactory.bring_accounts_online()
ac1.set_config("bcc_self", "1")
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message2")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message3")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_move_sync_msgs(acfactory):
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
acfactory.bring_accounts_online()
@@ -432,7 +318,7 @@ def test_forward_messages(acfactory, lp):
lp.sec("ac2: check new chat has a forwarded message")
assert chat3.is_promoted()
messages = chat3.get_messages()
assert len(messages) == 2
assert len(messages) == 3
msg = messages[-1]
assert msg.is_forwarded()
ac2.delete_messages(messages)
@@ -468,7 +354,7 @@ def test_forward_own_message(acfactory, lp):
def test_resend_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat1 = ac1.create_chat(ac2)
chat1 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: send message to ac2")
chat1.send_text("message")
@@ -476,14 +362,19 @@ def test_resend_message(acfactory, lp):
lp.sec("ac2: receive message")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message"
chat2 = msg_in.chat
chat2_msg_cnt = len(chat2.get_messages())
lp.sec("ac1: resend message")
ac1.resend_messages([msg_in])
lp.sec("ac2: check that message is deleted")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
lp.sec("ac1: send another message")
chat1.send_text("another message")
lp.sec("ac2: receive another message")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "another message"
chat2 = msg_in.chat
chat2_msg_cnt = len(chat2.get_messages())
assert len(chat2.get_messages()) == chat2_msg_cnt
@@ -610,39 +501,6 @@ def test_send_and_receive_message_markseen(acfactory, lp):
pass # mark_seen_messages() has generated events before it returns
def test_moved_markseen(acfactory):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
ac2.stop_io()
with ac2.direct_imap.idle() as idle2:
ac1.create_chat(ac2).send_text("Hello!")
idle2.wait_for_new_message()
# Emulate moving of the message to DeltaChat folder by Sieve rule.
ac2.direct_imap.conn.move(["*"], "DeltaChat")
ac2.direct_imap.select_folder("DeltaChat")
with ac2.direct_imap.idle() as idle2:
ac2.start_io()
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
# Accept the contact request.
msg.chat.accept()
ac2.mark_seen_messages([msg])
uid = idle2.wait_for_seen()
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*"))))) == 1
def test_message_override_sender_name(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("displayname", "ac1-default-displayname")
@@ -677,36 +535,6 @@ def test_message_override_sender_name(acfactory, lp):
assert not msg2.override_sender_name
@pytest.mark.parametrize("mvbox_move", [True, False])
def test_markseen_message_and_mdn(acfactory, mvbox_move):
# Please only change this test if you are very sure that it will still catch the issues it catches now.
# We had so many problems with markseen, if in doubt, rather create another test, it can't harm.
ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
ac2 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
acfactory.bring_accounts_online()
# Do not send BCC to self, we only want to test MDN on ac1.
ac1.set_config("bcc_self", "0")
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
ac2.mark_seen_messages([msg])
folder = "mvbox" if mvbox_move else "inbox"
for ac in [ac1, ac2]:
if mvbox_move:
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
else:
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
ac1.direct_imap.select_config_folder(folder)
ac2.direct_imap.select_config_folder(folder)
# Check that the mdn is marked as seen
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
# Check original message is marked as seen
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_reply_privately(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -856,156 +684,6 @@ def test_no_draft_if_cant_send(acfactory):
assert device_chat.get_draft() is None
def test_dont_show_emails(acfactory, lp):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown.
Also, test that unknown emails in the Spam folder are not shown."""
ac1 = acfactory.new_online_configuring_account()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.org").create_chat()
acfactory.wait_configured(ac1)
ac1.direct_imap.create_folder("Drafts")
ac1.direct_imap.create_folder("Sent")
ac1.direct_imap.create_folder("Spam")
ac1.direct_imap.create_folder("Junk")
acfactory.bring_accounts_online()
ac1.stop_io()
ac1.direct_imap.append(
"Drafts",
"""
From: ac1 <{}>
Subject: subj
To: alice@example.org
Message-ID: <aepiors@example.org>
Content-Type: text/plain; charset=utf-8
message in Drafts that is moved to Sent later
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
"Sent",
"""
From: ac1 <{}>
Subject: subj
To: alice@example.org
Message-ID: <hsabaeni@example.org>
Content-Type: text/plain; charset=utf-8
message in Sent
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org, unkwnown.add@junk.org
Subject: subj
To: {}
Message-ID: <spam.message2@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
"Spam",
"""
From: delta<address: inbox@nhroy.com>
Subject: subj
To: {}
Message-ID: <spam.message99@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
"Spam",
"""
From: alice@example.org
Subject: subj
To: {}
Message-ID: <spam.message3@junk.org>
Content-Type: text/plain; charset=utf-8
Actually interesting message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
"Junk",
"""
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Junk
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message")
ac1.start_io()
msg = ac1._evtracker.wait_next_messages_changed()
# Wait until each folder was scanned, this is necessary for this test to test what it should test:
ac1._evtracker.wait_idle_inbox_ready()
assert msg.text == "subj message in Sent"
chat_msgs = msg.chat.get_messages()
assert len(chat_msgs) == 2
assert any(msg.text == "subj Actually interesting message in Spam" for msg in chat_msgs)
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
ac1.direct_imap.select_folder("Spam")
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
ac1.stop_io()
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
ac1.direct_imap.select_folder("Drafts")
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
ac1.direct_imap.conn.move(uid, "Sent")
ac1.start_io()
msg2 = ac1._evtracker.wait_next_messages_changed()
assert msg2.text == "subj message in Drafts that is moved to Sent later"
assert len(msg.chat.get_messages()) == 3
def test_bot(acfactory, lp):
"""Test that bot messages can be identified as such"""
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1230,7 +908,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
that resulted in failure to propagate verification
when the database already contained the contact with a different email address capitalization.
"""
@@ -1241,24 +919,27 @@ def test_qr_email_capitalization(acfactory, lp):
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
ac1.create_contact(ac2_addr_uppercase)
lp.sec("ac3 creates a verified group with a QR code")
chat = ac3.create_group_chat("hello", verified=True)
lp.sec("ac3 creates a group with a QR code")
chat = ac3.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac1 joins a verified group via a QR code")
lp.sec("ac1 joins a group via a QR code")
ac1_chat = ac1.qr_join_chat(qr)
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
assert len(ac1_chat.get_contacts()) == 2
lp.sec("ac2 joins a verified group via a QR code")
lp.sec("ac2 joins a group via a QR code")
ac2.qr_join_chat(qr)
ac1._evtracker.wait_next_incoming_message()
# ac1 should see both ac3 and ac2 as verified.
assert len(ac1_chat.get_contacts()) == 3
# Until we reset verifications and then send the _verified header,
# the verification of ac2 is not gossiped here:
for contact in ac1_chat.get_contacts():
assert contact.is_verified()
is_ac2 = contact.addr == ac2.get_config("addr")
assert contact.is_verified() != is_ac2
def test_set_get_contact_avatar(acfactory, data, lp):
@@ -1525,9 +1206,15 @@ def test_send_receive_locations(acfactory, lp):
assert locations[0].latitude == 2.0
assert locations[0].longitude == 3.0
assert locations[0].accuracy == 0.5
assert locations[0].timestamp > now
assert locations[0].marker is None
# Make sure the timestamp is not in the past.
# Note that location timestamp has only 1 second precision,
# while `now` has a fractional part, so we have to truncate it
# first, otherwise `now` may appear to be in the future
# even though it is the same second.
assert int(locations[0].timestamp.timestamp()) >= int(now.timestamp())
contact = ac2.create_contact(ac1)
locations2 = chat2.get_locations(contact=contact)
assert len(locations2) == 1
@@ -1538,38 +1225,6 @@ def test_send_receive_locations(acfactory, lp):
assert not locations3
def test_immediate_autodelete(acfactory, lp):
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account()
acfactory.bring_accounts_online()
# "1" means delete immediately, while "0" means do not delete
ac2.set_config("delete_server_after", "1")
lp.sec("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
ac2.create_chat(ac1)
lp.sec("ac1: send message to ac2")
sent_msg = chat1.send_text("hello")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
lp.sec("ac2: wait for close/expunge on autodelete")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
lp.sec("ac2: check that message was autodeleted on server")
assert len(ac2.direct_imap.get_all_messages()) == 0
lp.sec("ac2: Mark deleted message as seen and check that read receipt arrives")
msg.mark_seen()
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 == chat1.id
assert ev.data2 == sent_msg.id
def test_delete_multiple_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat12 = acfactory.get_accepted_chat(ac1, ac2)
@@ -1602,55 +1257,6 @@ def test_delete_multiple_messages(acfactory, lp):
break
def test_trash_multiple_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.stop_io()
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
# Trash wasn't configured initially, it can't be configured later, let's check this.
lp.sec("Creating trash folder")
ac2.direct_imap.create_folder("Trash")
ac2.set_config("delete_to_trash", "1")
lp.sec("Check that Trash can be configured initially as well")
ac3 = acfactory.new_online_configuring_account(cloned_from=ac2)
acfactory.bring_accounts_online()
assert ac3.get_config("configured_trash_folder")
ac3.stop_io()
ac2.start_io()
chat12 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: sending 3 messages")
texts = ["first", "second", "third"]
for text in texts:
chat12.send_text(text)
lp.sec("ac2: waiting for all messages on the other side")
to_delete = []
for text in texts:
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text in texts
if text != "second":
to_delete.append(msg)
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
# check the configuration.
assert ac2.get_config("configured_trash_folder") == "Trash"
lp.sec("ac2: deleting all messages except second")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
lp.sec("ac2: test that only one message is left")
while 1:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac2.direct_imap.select_config_folder("inbox")
nr_msgs = len(ac2.direct_imap.get_all_messages())
assert nr_msgs > 0
if nr_msgs == 1:
break
def test_configure_error_msgs_wrong_pw(acfactory):
(ac1,) = acfactory.get_online_accounts(1)
@@ -1774,64 +1380,6 @@ def test_group_quote(acfactory, lp):
assert received_reply.quote.id == out_msg.id
@pytest.mark.parametrize(
("folder", "move", "expected_destination"),
[
(
"xyz",
False,
"xyz",
), # Test that emails are recognized in a random folder but not moved
(
"xyz",
True,
"DeltaChat",
), # ...emails are found in a random folder and moved to DeltaChat
(
"Spam",
False,
"INBOX",
), # ...emails are moved from the spam folder to the Inbox
],
)
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
def test_scan_folders(acfactory, lp, folder, move, expected_destination):
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
variant = folder + "-" + str(move) + "-" + expected_destination
lp.sec("Testing variant " + variant)
ac1 = acfactory.new_online_configuring_account(mvbox_move=move)
ac2 = acfactory.new_online_configuring_account()
acfactory.wait_configured(ac1)
ac1.direct_imap.create_folder(folder)
# Wait until each folder was selected once and we are IDLEing:
acfactory.bring_accounts_online()
ac1.stop_io()
assert folder in ac1.direct_imap.list_folders()
lp.sec("Send a message to from ac2 to ac1 and manually move it to the mvbox")
ac1.direct_imap.select_config_folder("inbox")
with ac1.direct_imap.idle() as idle1:
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
idle1.wait_for_new_message()
ac1.direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
lp.sec("start_io() and see if DeltaChat finds the message (" + variant + ")")
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.start_io()
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
# The message has been downloaded, which means it has reached its destination.
ac1.direct_imap.select_folder(expected_destination)
assert len(ac1.direct_imap.get_all_messages()) == 1
if folder != expected_destination:
ac1.direct_imap.select_folder(folder)
assert len(ac1.direct_imap.get_all_messages()) == 0
def test_archived_muted_chat(acfactory, lp):
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.

View File

@@ -271,10 +271,9 @@ class TestOfflineChat:
chat.set_name("Homework")
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
def test_group_chat_qr(self, acfactory, ac1):
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1", verified=verified)
chat = ac1.create_group_chat(name="title1")
assert chat.is_group()
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup
@@ -663,4 +662,4 @@ class TestOfflineChat:
lp.sec("check message count of only system messages (without daymarkers)")
sysmessages = [x for x in chat.get_messages() if x.is_system_message()]
assert len(sysmessages) == 3
assert len(sysmessages) == 4

View File

@@ -23,7 +23,6 @@ deps =
pytest
pytest-timeout
pytest-xdist
pdbpp
requests
# urllib3 2.0 does not work in manylinux2014 containers.
# https://github.com/deltachat/deltachat-core-rust/issues/4788

View File

@@ -1 +1 @@
2025-07-21
2025-11-11

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.88.0
RUST_VERSION=1.91.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=77cbf92a8565fdf1bcaba10fa93c1455c750a1e9
REV=d041136c19a48b493823b46d472f12b9ee94ae80
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"

20
spec.md
View File

@@ -1,6 +1,6 @@
# Chatmail Specification
Version: 0.36.0
Version: 0.37.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -582,6 +582,24 @@ and e.g. simply search for the line starting with `EMAIL`
in order to get the email address.
# Verifications
Keys obtained using [SecureJoin](https://securejoin.readthedocs.io) protocol
and corresponding contacts
are considered "verified".
As an extension to `Autocrypt-Gossip` header,
chatmail clients can add `_verified=1` attribute
(underscore marks the attribute as non-critical)
to indicate that they have the gossiped key
and the corresponding contact marked as verified.
When receiving such `Autocrypt-Gossip` header
in a message signed by a verified key,
chatmail clients mark the gossiped key
as indirectly verified.
# Transitioning to a new e-mail address (AEAP)
When receiving a message:

View File

@@ -18,7 +18,7 @@ use tokio::time::{Duration, sleep};
use crate::context::{Context, ContextBuilder};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::log::{info, warn};
use crate::log::warn;
use crate::push::PushSubscriber;
use crate::stock_str::StockStrings;
@@ -78,7 +78,7 @@ impl Accounts {
ensure!(dir.exists(), "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
ensure!(config_file.exists(), "{config_file:?} does not exist");
let config = Config::from_file(config_file, writable).await?;
let events = Events::new();
@@ -724,8 +724,7 @@ impl Config {
{
ensure!(
self.inner.accounts.iter().any(|e| e.id == id),
"invalid account id: {}",
id
"invalid account id: {id}"
);
self.inner.selected_account = id;

View File

@@ -17,7 +17,6 @@ pub enum EncryptPreference {
#[default]
NoPreference = 0,
Mutual = 1,
Reset = 20,
}
impl fmt::Display for EncryptPreference {
@@ -25,7 +24,6 @@ impl fmt::Display for EncryptPreference {
match *self {
EncryptPreference::Mutual => write!(fmt, "mutual"),
EncryptPreference::NoPreference => write!(fmt, "nopreference"),
EncryptPreference::Reset => write!(fmt, "reset"),
}
}
}
@@ -37,7 +35,7 @@ impl FromStr for EncryptPreference {
match s {
"mutual" => Ok(EncryptPreference::Mutual),
"nopreference" => Ok(EncryptPreference::NoPreference),
_ => bail!("Cannot parse encryption preference {}", s),
_ => bail!("Cannot parse encryption preference {s}"),
}
}
}
@@ -48,21 +46,13 @@ pub struct Aheader {
pub addr: String,
pub public_key: SignedPublicKey,
pub prefer_encrypt: EncryptPreference,
}
impl Aheader {
/// Creates new autocrypt header
pub fn new(
addr: String,
public_key: SignedPublicKey,
prefer_encrypt: EncryptPreference,
) -> Self {
Aheader {
addr,
public_key,
prefer_encrypt,
}
}
// Whether `_verified` attribute is present.
//
// `_verified` attribute is an extension to `Autocrypt-Gossip`
// header that is used to tell that the sender
// marked this key as verified.
pub verified: bool,
}
impl fmt::Display for Aheader {
@@ -71,6 +61,11 @@ impl fmt::Display for Aheader {
if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?;
}
// TODO After we reset all existing verifications,
// we want to start sending the _verified attribute
// if self.verified {
// write!(fmt, " _verified=1;")?;
// }
// adds a whitespace every 78 characters, this allows
// email crate to wrap the lines according to RFC 5322
@@ -125,6 +120,8 @@ impl FromStr for Aheader {
.and_then(|raw| raw.parse().ok())
.unwrap_or_default();
let verified = attributes.remove("_verified").is_some();
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
// Autocrypt-Level0: unknown attribute, treat the header as invalid
if attributes.keys().any(|k| !k.starts_with('_')) {
@@ -135,6 +132,7 @@ impl FromStr for Aheader {
addr,
public_key,
prefer_encrypt,
verified,
})
}
}
@@ -152,10 +150,11 @@ mod tests {
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(h.verified, false);
Ok(())
}
// EncryptPreference::Reset is an internal value, parser should never return it
// Non-standard values of prefer-encrypt such as `reset` are treated as no preference.
#[test]
fn test_from_str_reset() -> Result<()> {
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
@@ -245,11 +244,12 @@ mod tests {
assert!(
format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false
}
)
.contains("prefer-encrypt=mutual;")
);
@@ -260,11 +260,12 @@ mod tests {
assert!(
!format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::NoPreference
)
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: false
}
)
.contains("prefer-encrypt")
);
@@ -273,13 +274,28 @@ mod tests {
assert!(
format!(
"{}",
Aheader::new(
"TeSt@eXaMpLe.cOm".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
Aheader {
addr: "TeSt@eXaMpLe.cOm".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false
}
)
.contains("test@example.com")
);
// We don't send the _verified header yet:
assert!(
!format!(
"{}",
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: true
}
)
.contains("_verified")
);
}
}

View File

@@ -32,7 +32,7 @@ pub(crate) async fn handle_authres(
let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
}
};
@@ -468,7 +468,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
// The ordering in which the emails are received can matter;
// the test _should_ pass for every ordering.
dir.sort_by_key(|d| d.file_name());
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::thread_rng());
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::rng());
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;

View File

@@ -20,7 +20,7 @@ use crate::config::Config;
use crate::constants::{self, MediaQuality};
use crate::context::Context;
use crate::events::EventType;
use crate::log::{LogExt, error, info, warn};
use crate::log::{LogExt, warn};
use crate::message::Viewtype;
use crate::tools::sanitize_filename;
@@ -170,7 +170,7 @@ impl<'a> BlobObject<'a> {
false => name,
};
if !BlobObject::is_acceptible_blob_name(name) {
return Err(format_err!("not an acceptable blob name: {}", name));
return Err(format_err!("not an acceptable blob name: {name}"));
}
Ok(BlobObject {
blobdir: context.get_blobdir(),
@@ -234,8 +234,13 @@ impl<'a> BlobObject<'a> {
/// If `data` represents an image of known format, this adds the corresponding extension.
///
/// Even though this function is not async, it's OK to call it from an async context.
pub(crate) fn store_from_base64(context: &Context, data: &str) -> Result<String> {
let buf = base64::engine::general_purpose::STANDARD.decode(data)?;
///
/// Returns an error if there is an I/O problem,
/// but in case of a failure to decode base64 returns `Ok(None)`.
pub(crate) fn store_from_base64(context: &Context, data: &str) -> Result<Option<String>> {
let Ok(buf) = base64::engine::general_purpose::STANDARD.decode(data) else {
return Ok(None);
};
let name = if let Ok(format) = image::guess_format(&buf) {
if let Some(ext) = format.extensions_str().first() {
format!("file.{ext}")
@@ -246,7 +251,7 @@ impl<'a> BlobObject<'a> {
String::new()
};
let blob = BlobObject::create_and_deduplicate_from_bytes(context, &buf, &name)?;
Ok(blob.as_name().to_string())
Ok(Some(blob.as_name().to_string()))
}
/// Recode image to avatar size.
@@ -367,11 +372,12 @@ impl<'a> BlobObject<'a> {
|| img.get_pixel(x_max, y_max).0[3] == 0)
{
*vt = Viewtype::Image;
} else {
// Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming
// from UIs shouldn't contain sensitive Exif info.
return Ok(name);
}
}
if *vt == Viewtype::Sticker && exif.is_none() {
return Ok(name);
}
img = match orientation {
Some(90) => img.rotate90(),
@@ -457,8 +463,7 @@ impl<'a> BlobObject<'a> {
{
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B.",
max_bytes,
"Failed to scale image to below {max_bytes}B.",
));
}
@@ -537,7 +542,11 @@ fn file_hash(src: &Path) -> Result<blake3::Hash> {
fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(file);
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
let exif = exif::Reader::new()
.continue_on_error(true)
.read_from_container(&mut bufreader)
.or_else(|e| e.distill_partial_result(|_errors| {}))
.ok();
Ok((len, exif))
}

View File

@@ -334,6 +334,28 @@ async fn test_recode_image_2() {
assert_correct_rotation(&img_rotated);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_bad_exif() {
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
// detected and removed.
let bytes = include_bytes!("../../test-data/image/1000x1000-bad-exif.jpg");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
bytes,
extension: "jpg",
has_exif: true,
original_width: 1000,
original_height: 1000,
compressed_width: 1000,
compressed_height: 1000,
..Default::default()
}
.test()
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_balanced_png() {
let bytes = include_bytes!("../../test-data/image/screenshot.png");
@@ -416,6 +438,28 @@ async fn test_recode_image_balanced_png() {
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_with_exif() {
let bytes = include_bytes!("../../test-data/image/logo-exif.png");
SendImageCheckMediaquality {
viewtype: Viewtype::Sticker,
bytes,
extension: "png",
// TODO: Pretend there's no Exif. Currently `exif` crate doesn't detect Exif in this image,
// so the test doesn't check all the logic it should.
has_exif: false,
original_width: 135,
original_height: 135,
res_viewtype: Some(Viewtype::Sticker),
compressed_width: 135,
compressed_height: 135,
..Default::default()
}
.test()
.await
.unwrap();
}
/// Tests that RGBA PNG can be recoded into JPEG
/// by dropping alpha channel.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -485,6 +529,7 @@ struct SendImageCheckMediaquality<'a> {
pub(crate) original_width: u32,
pub(crate) original_height: u32,
pub(crate) orientation: i32,
pub(crate) res_viewtype: Option<Viewtype>,
pub(crate) compressed_width: u32,
pub(crate) compressed_height: u32,
pub(crate) set_draft: bool,
@@ -500,6 +545,7 @@ impl SendImageCheckMediaquality<'_> {
let original_width = self.original_width;
let original_height = self.original_height;
let orientation = self.orientation;
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
let compressed_width = self.compressed_width;
let compressed_height = self.compressed_height;
let set_draft = self.set_draft;
@@ -550,7 +596,7 @@ impl SendImageCheckMediaquality<'_> {
}
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_viewtype(), res_viewtype);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file_saved = bob
@@ -564,7 +610,7 @@ impl SendImageCheckMediaquality<'_> {
}
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
assert!(exif.is_none());
assert!(res_viewtype != Viewtype::Image || exif.is_none());
let img = check_image_size(file_saved, compressed_width, compressed_height);

702
src/calls.rs Normal file
View File

@@ -0,0 +1,702 @@
//! # Handle calls.
//!
//! Internally, calls are bound a user-visible message initializing the call.
//! 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::constants::{Blocked, Chattype};
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::log::warn;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::net::dns::lookup_host_with_cache;
use crate::param::Param;
use crate::tools::time;
use anyhow::{Context as _, Result, ensure};
use sdp::SessionDescription;
use serde::Serialize;
use std::io::Cursor;
use std::str::FromStr;
use std::time::Duration;
use tokio::task;
use tokio::time::sleep;
/// How long callee's or caller's phone ring.
///
/// For the callee, this is to prevent endless ringing
/// in case the initial "call" is received, but then the caller went offline.
/// Moreover, this prevents outdated calls to ring
/// in case the initial "call" message arrives delayed.
///
/// 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;
// For persisting parameters in the call, we use Param::Arg*
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
const STUN_PORT: u16 = 3478;
/// Set if incoming call was ended explicitly
/// by the other side before we accepted it.
///
/// It is used to distinguish "ended" calls
/// that are rejected by us from the calls
/// canceled by the other side
/// immediately after ringing started.
const CALL_CANCELED_TIMESTAMP: Param = Param::Arg2;
/// Information about the status of a call.
#[derive(Debug, Default)]
pub struct CallInfo {
/// User-defined text as given to place_outgoing_call()
pub place_call_info: String,
/// User-defined text as given to accept_incoming_call()
pub accept_call_info: String,
/// Message referring to the call.
/// Data are persisted along with the message using Param::Arg*
pub msg: Message,
}
impl CallInfo {
/// Returns true if the call is an incoming call.
pub fn is_incoming(&self) -> bool {
self.msg.from_id != ContactId::SELF
}
/// Returns true if the call should not ring anymore.
pub fn is_stale(&self) -> bool {
(self.is_incoming() || self.msg.timestamp_sent != 0) && self.remaining_ring_seconds() <= 0
}
fn remaining_ring_seconds(&self) -> i64 {
let remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time();
remaining_seconds.clamp(0, RINGING_SECONDS)
}
async fn update_text(&self, context: &Context, text: &str) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET txt=?, txt_normalized=? WHERE id=?",
(text, message::normalize_text(text), self.msg.id),
)
.await?;
Ok(())
}
async fn update_text_duration(&self, context: &Context) -> Result<()> {
let minutes = self.duration_seconds() / 60;
let duration = match minutes {
0 => "<1 minute".to_string(),
1 => "1 minute".to_string(),
n => format!("{n} minutes"),
};
if self.is_incoming() {
self.update_text(context, &format!("Incoming call\n{duration}"))
.await?;
} else {
self.update_text(context, &format!("Outgoing call\n{duration}"))
.await?;
}
Ok(())
}
/// Mark calls as accepted.
/// This is needed for all devices where a stale-timer runs, to prevent accepted calls being terminated as stale.
async fn mark_as_accepted(&mut self, context: &Context) -> Result<()> {
self.msg.param.set_i64(CALL_ACCEPTED_TIMESTAMP, time());
self.msg.update_param(context).await?;
Ok(())
}
/// Returns true if the call is accepted.
pub fn is_accepted(&self) -> bool {
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
}
/// Returns true if the call is missed
/// because the caller canceled it
/// explicitly before ringing stopped.
///
/// For outgoing calls this means
/// the receiver has rejected the call
/// explicitly.
pub fn is_canceled(&self) -> bool {
self.msg.param.exists(CALL_CANCELED_TIMESTAMP)
}
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time());
self.msg.update_param(context).await?;
Ok(())
}
/// Explicitly mark the call as canceled.
///
/// For incoming calls this should be called
/// when "call ended" message is received
/// from the caller before we picked up the call.
/// In this case the call becomes "missed" early
/// before the ringing timeout.
async fn mark_as_canceled(&mut self, context: &Context) -> Result<()> {
let now = time();
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, now);
self.msg.param.set_i64(CALL_CANCELED_TIMESTAMP, now);
self.msg.update_param(context).await?;
Ok(())
}
/// Returns true if the call is ended.
pub fn is_ended(&self) -> bool {
self.msg.param.exists(CALL_ENDED_TIMESTAMP)
}
/// Returns call duration in seconds.
pub fn duration_seconds(&self) -> i64 {
if let (Some(start), Some(end)) = (
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
self.msg.param.get_i64(CALL_ENDED_TIMESTAMP),
) {
let seconds = end - start;
if seconds <= 0 {
return 1;
}
return seconds;
}
0
}
}
impl Context {
/// Start an outgoing call.
pub async fn place_outgoing_call(
&self,
chat_id: ChatId,
place_call_info: String,
) -> Result<MsgId> {
let chat = Chat::load_from_db(self, chat_id).await?;
ensure!(
chat.typ == Chattype::Single,
"Can only place calls in 1:1 chats"
);
ensure!(!chat.is_self_talk(), "Cannot call self");
let mut call = Message {
viewtype: Viewtype::Call,
text: "Outgoing call".into(),
..Default::default()
};
call.param.set(Param::WebrtcRoom, &place_call_info);
call.id = send_msg(self, chat_id, &mut call).await?;
let wait = RINGING_SECONDS;
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
wait.try_into()?,
call.id,
));
Ok(call.id)
}
/// Accept an incoming call.
pub async fn accept_incoming_call(
&self,
call_id: MsgId,
accept_call_info: String,
) -> Result<()> {
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
format!("accept_incoming_call is called with {call_id} which does not refer to a call")
})?;
ensure!(call.is_incoming());
if call.is_accepted() || call.is_ended() {
info!(self, "Call already accepted/ended");
return Ok(());
}
call.mark_as_accepted(self).await?;
let chat = Chat::load_from_db(self, call.msg.chat_id).await?;
if chat.is_contact_request() {
chat.id.accept(self).await?;
}
// send an acceptance message around: to the caller as well as to the other devices of the callee
let mut msg = Message {
viewtype: Viewtype::Text,
text: "[Call accepted]".into(),
..Default::default()
};
msg.param.set_cmd(SystemMessage::CallAccepted);
msg.hidden = true;
msg.param
.set(Param::WebrtcAccepted, accept_call_info.to_string());
msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
self.emit_event(EventType::IncomingCallAccepted {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(())
}
/// Cancel, decline or hangup an incoming or outgoing call.
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
format!("end_call is called with {call_id} which does not refer to a call")
})?;
if call.is_ended() {
info!(self, "Call already ended");
return Ok(());
}
if !call.is_accepted() {
if call.is_incoming() {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
} else {
call.mark_as_canceled(self).await?;
call.update_text(self, "Canceled call").await?;
}
} else {
call.mark_as_ended(self).await?;
call.update_text_duration(self).await?;
}
let mut msg = Message {
viewtype: Viewtype::Text,
text: "[Call ended]".into(),
..Default::default()
};
msg.param.set_cmd(SystemMessage::CallEnded);
msg.hidden = true;
msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(())
}
async fn emit_end_call_if_unaccepted(
context: Context,
wait: u64,
call_id: MsgId,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let Some(mut call) = context.load_call_by_id(call_id).await? else {
warn!(
context,
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
);
return Ok(());
};
if !call.is_accepted() && !call.is_ended() {
if call.is_incoming() {
call.mark_as_canceled(&context).await?;
call.update_text(&context, "Missed call").await?;
} else {
call.mark_as_ended(&context).await?;
call.update_text(&context, "Canceled call").await?;
}
context.emit_msgs_changed(call.msg.chat_id, call_id);
context.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
}
Ok(())
}
pub(crate) async fn handle_call_msg(
&self,
call_id: MsgId,
mime_message: &MimeMessage,
from_id: ContactId,
) -> Result<()> {
if mime_message.is_call() {
let Some(call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
if call.is_incoming() {
if call.is_stale() {
call.update_text(self, "Missed call").await?;
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
} else {
call.update_text(self, "Incoming call").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
let has_video = match sdp_has_video(&call.place_call_info) {
Ok(has_video) => has_video,
Err(err) => {
warn!(self, "Failed to determine if SDP offer has video: {err:#}.");
false
}
};
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 wait = call.remaining_ring_seconds();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
wait.try_into()?,
call.msg.id,
));
}
} else {
call.update_text(self, "Outgoing call").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
}
} else {
match mime_message.is_system_message {
SystemMessage::CallAccepted => {
let Some(mut call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
if call.is_ended() || call.is_accepted() {
info!(self, "CallAccepted received for accepted/ended call");
return Ok(());
}
call.mark_as_accepted(self).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
if call.is_incoming() {
self.emit_event(EventType::IncomingCallAccepted {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
} else {
let accept_call_info = mime_message
.get_header(HeaderDef::ChatWebrtcAccepted)
.unwrap_or_default();
self.emit_event(EventType::OutgoingCallAccepted {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
accept_call_info: accept_call_info.to_string(),
});
}
}
SystemMessage::CallEnded => {
let Some(mut call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
if call.is_ended() {
// may happen eg. if a a message is missed
info!(self, "CallEnded received for ended call");
return Ok(());
}
if !call.is_accepted() {
if call.is_incoming() {
if from_id == ContactId::SELF {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
} else {
call.mark_as_canceled(self).await?;
call.update_text(self, "Missed call").await?;
}
} else {
// outgoing
if from_id == ContactId::SELF {
call.mark_as_canceled(self).await?;
call.update_text(self, "Canceled call").await?;
} else {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
}
}
} else {
call.mark_as_ended(self).await?;
call.update_text_duration(self).await?;
}
self.emit_msgs_changed(call.msg.chat_id, call_id);
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
}
_ => {}
}
}
Ok(())
}
/// Loads information about the call given its ID.
///
/// If the message referred to by ID is
/// not a call message, returns `None`.
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<Option<CallInfo>> {
let call = Message::load_from_db(self, call_id).await?;
Ok(self.load_call_by_message(call))
}
// Loads information about the call given the `Message`.
//
// If the `Message` is not a call message, returns `None`
fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
if call.viewtype != Viewtype::Call {
// This can happen e.g. if a "call accepted"
// or "call ended" message is received
// with `In-Reply-To` referring to non-call message.
return None;
}
Some(CallInfo {
place_call_info: call
.param
.get(Param::WebrtcRoom)
.unwrap_or_default()
.to_string(),
accept_call_info: call
.param
.get(Param::WebrtcAccepted)
.unwrap_or_default()
.to_string(),
msg: call,
})
}
}
/// Returns true if SDP offer has a video.
pub fn sdp_has_video(sdp: &str) -> Result<bool> {
let mut cursor = Cursor::new(sdp);
let session_description =
SessionDescription::unmarshal(&mut cursor).context("Failed to parse SDP")?;
for media_description in &session_description.media_descriptions {
if media_description.media_name.media == "video" {
return Ok(true);
}
}
Ok(false)
}
/// State of the call for display in the message bubble.
#[derive(Debug, PartialEq, Eq)]
pub enum CallState {
/// Fresh incoming or outgoing call that is still ringing.
///
/// There is no separate state for outgoing call
/// that has been dialled but not ringing on the other side yet
/// as we don't know whether the other side received our call.
Alerting,
/// Active call.
Active,
/// Completed call that was once active
/// and then was terminated for any reason.
Completed {
/// Call duration in seconds.
duration: i64,
},
/// Incoming call that was not picked up within a timeout
/// or was explicitly ended by the caller before we picked up.
Missed,
/// Incoming call that was explicitly ended on our side
/// before picking up or outgoing call
/// that was declined before the timeout.
Declined,
/// Outgoing call that has been canceled on our side
/// before receiving a response.
///
/// Incoming calls cannot be canceled,
/// on the receiver side canceled calls
/// usually result in missed calls.
Canceled,
}
/// Returns call state given the message ID.
///
/// Returns an error if the message is not a call message.
pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
let call = context
.load_call_by_id(msg_id)
.await?
.with_context(|| format!("{msg_id} is not a call message"))?;
let state = if call.is_incoming() {
if call.is_accepted() {
if call.is_ended() {
CallState::Completed {
duration: call.duration_seconds(),
}
} else {
CallState::Active
}
} else if call.is_canceled() {
// Call was explicitly canceled
// by the caller before we picked it up.
CallState::Missed
} else if call.is_ended() {
CallState::Declined
} else if call.is_stale() {
CallState::Missed
} else {
CallState::Alerting
}
} else if call.is_accepted() {
if call.is_ended() {
CallState::Completed {
duration: call.duration_seconds(),
}
} else {
CallState::Active
}
} else if call.is_canceled() {
CallState::Canceled
} else if call.is_ended() || call.is_stale() {
CallState::Declined
} else {
CallState::Alerting
};
Ok(state)
}
/// ICE server for JSON serialization.
#[derive(Serialize, Debug, Clone, PartialEq)]
struct IceServer {
/// STUN or TURN URLs.
pub urls: Vec<String>,
/// Username for TURN server authentication.
pub username: Option<String>,
/// Password for logging into the server.
pub credential: Option<String>,
}
/// Creates JSON with ICE servers.
async fn create_ice_servers(
context: &Context,
hostname: &str,
port: u16,
username: &str,
password: &str,
) -> Result<String> {
// Do not use cache because there is no TLS.
let load_cache = false;
let urls: Vec<String> = lookup_host_with_cache(context, hostname, port, "", load_cache)
.await?
.into_iter()
.map(|addr| format!("turn:{addr}"))
.collect();
let ice_server = IceServer {
urls,
username: Some(username.to_string()),
credential: Some(password.to_string()),
};
let json = serde_json::to_string(&[ice_server])?;
Ok(json)
}
/// Creates JSON with ICE servers from a line received over IMAP METADATA.
///
/// IMAP METADATA returns a line such as
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
///
/// 1758650868 is the username and expiration timestamp
/// at the same time,
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
/// is the password.
pub(crate) async fn create_ice_servers_from_metadata(
context: &Context,
metadata: &str,
) -> Result<(i64, String)> {
let (hostname, rest) = metadata.split_once(':').context("Missing hostname")?;
let (port, rest) = rest.split_once(':').context("Missing port")?;
let port = u16::from_str(port).context("Failed to parse the port")?;
let (ts, password) = rest.split_once(':').context("Missing timestamp")?;
let expiration_timestamp = i64::from_str(ts).context("Failed to parse the timestamp")?;
let ice_servers = create_ice_servers(context, hostname, port, ts, password).await?;
Ok((expiration_timestamp, ice_servers))
}
/// Creates JSON with ICE servers when no TURN servers are known.
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
// Do not use public STUN server from https://stunprotocol.org/.
// It changes the hostname every year
// (e.g. stunserver2025.stunprotocol.org
// which was previously stunserver2024.stunprotocol.org)
// 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)
.await?
.into_iter()
.map(|addr| format!("stun:{addr}"))
.collect();
let ice_server = IceServer {
urls,
username: None,
credential: None,
};
let json = serde_json::to_string(&[ice_server])?;
Ok(json)
}
/// Returns JSON with ICE servers.
///
/// <https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers>
///
/// All returned servers are resolved to their IP addresses.
/// The primary point of DNS lookup is that Delta Chat Desktop
/// relies on the servers being specified by IP,
/// because it itself cannot utilize DNS. See
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
pub async fn ice_servers(context: &Context) -> Result<String> {
if let Some(ref metadata) = *context.metadata.read().await {
Ok(metadata.ice_servers.clone())
} else {
Ok("[]".to_string())
}
}
#[cfg(test)]
mod calls_tests;

674
src/calls/calls_tests.rs Normal file
View File

@@ -0,0 +1,674 @@
use super::*;
use crate::chat::forward_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::test_utils::{TestContext, TestContextManager};
struct CallSetup {
pub alice: TestContext,
pub alice2: TestContext,
pub alice_call: Message,
pub alice2_call: Message,
pub bob: TestContext,
pub bob2: TestContext,
pub bob_call: Message,
pub bob2_call: Message,
}
async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()> {
assert_eq!(Message::load_from_db(t, call_id).await?.text, text);
Ok(())
}
// Offer and answer examples from <https://www.rfc-editor.org/rfc/rfc3264>
const PLACE_INFO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\ns=-\r\nc=IN IP4 host.anywhere.com\r\nt=0 0\r\nm=audio 62986 RTP/AVP 0 4 18\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=rtpmap:18 G729/8000\r\na=inactive\r\n";
const ACCEPT_INFO: &str = "v=0\r\no=bob 2890844730 2890844731 IN IP4 host.example.com\r\ns=\r\nc=IN IP4 host.example.com\r\nt=0 0\r\nm=audio 54344 RTP/AVP 0 4\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=inactive\r\n";
/// Example from <https://datatracker.ietf.org/doc/rfc9143/>
/// with `s= ` replaced with `s=-`.
///
/// `s=` cannot be empty according to RFC 3264,
/// so it is more clear as `s=-`.
const PLACE_INFO_VIDEO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP6 2001:db8::3\r\ns=-\r\nc=IN IP6 2001:db8::3\r\nt=0 0\r\na=group:BUNDLE foo bar\r\n\r\nm=audio 10000 RTP/AVP 0 8 97\r\nb=AS:200\r\na=mid:foo\r\na=rtcp-mux\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:97 iLBC/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\r\nm=video 10002 RTP/AVP 31 32\r\nb=AS:1000\r\na=mid:bar\r\na=rtcp-mux\r\na=rtpmap:31 H261/90000\r\na=rtpmap:32 MPV/90000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n";
async fn setup_call() -> Result<CallSetup> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let alice2 = tcm.alice().await;
let bob = tcm.bob().await;
let bob2 = tcm.bob().await;
for t in [&alice, &alice2, &bob, &bob2] {
t.set_config_bool(Config::SyncMsgs, true).await?;
}
// Alice creates a chat with Bob and places an outgoing call there.
// Alice's other device sees the same message as an outgoing call.
let alice_chat = alice.create_chat(&bob).await;
// Create chat on Bob's side
// so incoming call causes a notification.
bob.create_chat(&alice).await;
bob2.create_chat(&alice).await;
let test_msg_id = alice
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
.await?;
let sent1 = alice.pop_sent_msg().await;
assert_eq!(sent1.sender_msg_id, test_msg_id);
let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?;
let alice2_call = alice2.recv_msg(&sent1).await;
for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] {
assert!(!m.is_info());
assert_eq!(m.viewtype, Viewtype::Call);
let info = t
.load_call_by_id(m.id)
.await?
.expect("m should be a call message");
assert!(!info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_text(t, m.id, "Outgoing call").await?;
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
}
// Bob receives the message referring to the call on two devices;
// it is an incoming call from the view of Bob
let bob_call = bob.recv_msg(&sent1).await;
let bob2_call = bob2.recv_msg(&sent1).await;
for (t, m) in [(&bob, &bob_call), (&bob2, &bob2_call)] {
assert!(!m.is_info());
assert_eq!(m.viewtype, Viewtype::Call);
t.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
.await;
let info = t
.load_call_by_id(m.id)
.await?
.expect("IncomingCall event should refer to a call message");
assert!(info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_text(t, m.id, "Incoming call").await?;
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
}
Ok(CallSetup {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
})
}
async fn accept_call() -> Result<CallSetup> {
let CallSetup {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
} = setup_call().await?;
// Bob accepts the incoming call
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
.await?;
assert_text(&bob, bob_call.id, "Incoming call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let sent2 = bob.pop_sent_msg().await;
let info = bob
.load_call_by_id(bob_call.id)
.await?
.expect("bob_call should be a call message");
assert!(info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
bob2.recv_msg_trash(&sent2).await;
assert_text(&bob, bob_call.id, "Incoming call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let info = bob2
.load_call_by_id(bob2_call.id)
.await?
.expect("bob2_call should be a call message");
assert!(info.is_accepted());
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Active);
// Alice receives the acceptance message
alice.recv_msg_trash(&sent2).await;
assert_text(&alice, alice_call.id, "Outgoing call").await?;
let ev = alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
.await;
assert_eq!(
ev,
EventType::OutgoingCallAccepted {
msg_id: alice_call.id,
chat_id: alice_call.chat_id,
accept_call_info: ACCEPT_INFO.to_string()
}
);
let info = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
assert!(info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
alice2.recv_msg_trash(&sent2).await;
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
.await;
assert_eq!(
call_state(&alice2, alice2_call.id).await?,
CallState::Active
);
Ok(CallSetup {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
})
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_accept_call_callee_ends() -> Result<()> {
// Alice calls Bob, Bob accepts
let CallSetup {
alice,
alice_call,
alice2,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = accept_call().await?;
// Bob has accepted the call and also ends it
bob.end_call(bob_call.id).await?;
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
assert!(matches!(
call_state(&bob, bob_call.id).await?,
CallState::Completed { .. }
));
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&bob2, bob2_call.id).await?,
CallState::Completed { .. }
));
// Alice receives the ending message
alice.recv_msg_trash(&sent3).await;
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&alice, alice_call.id).await?,
CallState::Completed { .. }
));
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&alice2, alice2_call.id).await?,
CallState::Completed { .. }
));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_accept_call_caller_ends() -> Result<()> {
// Alice calls Bob, Bob accepts
let CallSetup {
alice,
alice_call,
alice2,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = accept_call().await?;
// Bob has accepted the call but Alice ends it
alice.end_call(alice_call.id).await?;
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = alice.pop_sent_msg().await;
assert!(matches!(
call_state(&alice, alice_call.id).await?,
CallState::Completed { .. }
));
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&alice2, alice2_call.id).await?,
CallState::Completed { .. }
));
// Bob receives the ending message
bob.recv_msg_trash(&sent3).await;
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&bob, bob_call.id).await?,
CallState::Completed { .. }
));
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&bob2, bob2_call.id).await?,
CallState::Completed { .. }
));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_callee_rejects_call() -> Result<()> {
// Alice calls Bob
let CallSetup {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = setup_call().await?;
// Bob has accepted Alice before, but does not want to talk with Alice
bob_call.chat_id.accept(&bob).await?;
bob.end_call(bob_call.id).await?;
assert_text(&bob, bob_call.id, "Declined call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Declined);
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Declined call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Declined);
// Alice receives decline message
alice.recv_msg_trash(&sent3).await;
assert_text(&alice, alice_call.id, "Declined call").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(
call_state(&alice, alice_call.id).await?,
CallState::Declined
);
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Declined call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(
call_state(&alice2, alice2_call.id).await?,
CallState::Declined
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_caller_cancels_call() -> Result<()> {
// Alice calls Bob
let CallSetup {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = setup_call().await?;
// Alice changes their mind before Bob picks up
alice.end_call(alice_call.id).await?;
assert_text(&alice, alice_call.id, "Canceled call").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = alice.pop_sent_msg().await;
assert_eq!(
call_state(&alice, alice_call.id).await?,
CallState::Canceled
);
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Canceled call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(
call_state(&alice2, alice2_call.id).await?,
CallState::Canceled
);
// Bob receives the ending message
bob.recv_msg_trash(&sent3).await;
assert_text(&bob, bob_call.id, "Missed call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Missed);
// Test that message summary says it is a missed call.
let bob_call_msg = Message::load_from_db(&bob, bob_call.id).await?;
let summary = bob_call_msg.get_summary(&bob, None).await?;
assert_eq!(summary.text, "📞 Missed call");
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Missed call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Missed);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_is_stale_call() -> Result<()> {
// a call started now is not stale
let call_info = CallInfo {
msg: Message {
timestamp_sent: time(),
..Default::default()
},
..Default::default()
};
assert!(!call_info.is_stale());
let remaining_seconds = call_info.remaining_ring_seconds();
assert!(remaining_seconds == RINGING_SECONDS || remaining_seconds == RINGING_SECONDS - 1);
// call started 5 seconds ago, this is not stale as well
let call_info = CallInfo {
msg: Message {
timestamp_sent: time() - 5,
..Default::default()
},
..Default::default()
};
assert!(!call_info.is_stale());
let remaining_seconds = call_info.remaining_ring_seconds();
assert!(remaining_seconds == RINGING_SECONDS - 5 || remaining_seconds == RINGING_SECONDS - 6);
// a call started one hour ago is clearly stale
let call_info = CallInfo {
msg: Message {
timestamp_sent: time() - 3600,
..Default::default()
},
..Default::default()
};
assert!(call_info.is_stale());
assert_eq!(call_info.remaining_ring_seconds(), 0);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mark_calls() -> Result<()> {
let CallSetup {
alice, alice_call, ..
} = setup_call().await?;
let mut call_info: CallInfo = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
assert!(!call_info.is_accepted());
assert!(!call_info.is_ended());
call_info.mark_as_accepted(&alice).await?;
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
let mut call_info: CallInfo = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
call_info.mark_as_ended(&alice).await?;
assert!(call_info.is_accepted());
assert!(call_info.is_ended());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_call_text() -> Result<()> {
let CallSetup {
alice, alice_call, ..
} = setup_call().await?;
let call_info = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
call_info.update_text(&alice, "foo bar").await?;
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
assert_eq!(alice_call.get_text(), "foo bar");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sdp_has_video() {
assert!(sdp_has_video("foobar").is_err());
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
}
/// Tests that calls are forwarded as text messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_call() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_bob_chat = alice.create_chat(bob).await;
let alice_msg_id = alice
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
.await
.context("Failed to place a call")?;
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
let _alice_sent_call = alice.pop_sent_msg().await;
assert_eq!(alice_call.viewtype, Viewtype::Call);
let alice_charlie_chat = alice.create_chat(charlie).await;
forward_msgs(alice, &[alice_call.id], alice_charlie_chat.id).await?;
let alice_forwarded_call = alice.pop_sent_msg().await;
let alice_forwarded_call_msg = alice_forwarded_call.load_from_db().await;
assert_eq!(alice_forwarded_call_msg.viewtype, Viewtype::Text);
let charlie_forwarded_call = charlie.recv_msg(&alice_forwarded_call).await;
assert_eq!(charlie_forwarded_call.viewtype, Viewtype::Text);
Ok(())
}
/// Tests that "end call" message referring
/// to a text message does not make receive_imf fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_end_text_call() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let received1 = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <first@example.net>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
Chat-Version: 1.0\n\
\n\
Hello\n",
false,
)
.await?
.unwrap();
assert_eq!(received1.msg_ids.len(), 1);
let msg = Message::load_from_db(alice, received1.msg_ids[0])
.await
.unwrap();
assert_eq!(msg.viewtype, Viewtype::Text);
// Receiving "Call ended" message that refers
// to the text message does not result in an error.
let received2 = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <second@example.net>\n\
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
In-Reply-To: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call-ended\n\
\n\
Call ended\n",
false,
)
.await?
.unwrap();
assert_eq!(received2.msg_ids.len(), 1);
assert_eq!(received2.chat_id, DC_CHAT_ID_TRASH);
Ok(())
}
/// Tests that partially downloaded "call ended"
/// messages are not processed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_partial_calls() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let seen = false;
// The messages in the test
// have no `Date` on purpose,
// so they are treated as new.
let received_call = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call\n\
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
\n\
Hello, this is a call\n",
seen,
)
.await?
.unwrap();
assert_eq!(received_call.msg_ids.len(), 1);
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
.await
.unwrap();
assert_eq!(call_msg.viewtype, Viewtype::Call);
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
let imf_raw = b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <second@example.net>\n\
In-Reply-To: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call-ended\n\
\n\
Call ended\n";
receive_imf_from_inbox(
alice,
"second@example.net",
imf_raw,
seen,
Some(imf_raw.len().try_into().unwrap()),
)
.await?;
// The call is still not ended.
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
// Fully downloading the message ends the call.
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
.await
.context("Failed to fully download end call message")?;
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
Ok(())
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -107,11 +107,6 @@ impl Chatlist {
Ok((chat_id, msg_id))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
let skip_id = if flag_for_forwarding {
ChatId::lookup_by_contact(context, ContactId::DEVICE)
.await?
@@ -132,7 +127,7 @@ impl Chatlist {
// groups. Otherwise it would be hard to follow conversations.
let ids = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.query_map(
context.sql.query_map_vec(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -150,7 +145,6 @@ impl Chatlist {
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
process_row,
process_rows,
).await?
} else if flag_archived_only {
// show archived chats
@@ -159,7 +153,7 @@ impl Chatlist {
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context
.sql
.query_map(
.query_map_vec(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -177,7 +171,6 @@ impl Chatlist {
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft,),
process_row,
process_rows,
)
.await?
} else if let Some(query) = query {
@@ -195,7 +188,7 @@ impl Chatlist {
let str_like_cmd = format!("%{query}%");
context
.sql
.query_map(
.query_map_vec(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -214,7 +207,6 @@ impl Chatlist {
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
process_row,
process_rows,
)
.await?
} else {
@@ -229,7 +221,7 @@ impl Chatlist {
let msg_id: Option<MsgId> = row.get(3)?;
Ok((chat_id, typ, param, msg_id))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
let process_rows = |rows: rusqlite::AndThenRows<_>| {
rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
Ok((chat_id, typ, param, msg_id)) => {
if typ == Chattype::Mailinglist
@@ -243,11 +235,7 @@ impl Chatlist {
Err(e) => Some(Err(e)),
})
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
// time. It may be confusing if a chat that is normally in the list disappears
// suddenly. The UI need to deal with that case anyway.
context.sql.query_map(
"SELECT c.id, c.type, c.param, m.id
FROM chats c
@@ -275,7 +263,7 @@ impl Chatlist {
).await?
} else {
// show normal chatlist
context.sql.query_map(
context.sql.query_map_vec(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -293,7 +281,6 @@ impl Chatlist {
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
process_row,
process_rows,
).await?
};
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
@@ -484,27 +471,23 @@ mod tests {
use super::*;
use crate::chat::save_msgs;
use crate::chat::{
ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts,
remove_contact_from_chat, send_text_msg,
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
send_text_msg,
};
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools::SystemTime;
use std::time::Duration;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
let chat_id1 = create_group(bob, "a chat").await.unwrap();
let chat_id2 = create_group(bob, "b chat").await.unwrap();
let chat_id3 = create_group(bob, "c chat").await.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
@@ -513,6 +496,8 @@ mod tests {
assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
assert_eq!(chats.get_chat_id(2).unwrap(), chat_id1);
SystemTime::shift(Duration::from_secs(5));
// New drafts are sorted to the top
// We have to set a draft on the other two messages, too, as
// chat timestamps are only exact to the second and sorting by timestamp
@@ -535,9 +520,7 @@ mod tests {
// receive a message from alice
let alice = &tcm.alice().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
.await
.unwrap();
let alice_chat_id = create_group(alice, "alice chat").await.unwrap();
add_contact_to_chat(
alice,
alice_chat_id,
@@ -575,9 +558,7 @@ mod tests {
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new_alice().await;
t.update_device_chats().await.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
create_group(&t, "a chat").await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
@@ -764,9 +745,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id1 = create_group(&t, "a chat").await.unwrap();
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
@@ -782,9 +761,7 @@ mod tests {
async fn test_get_summary_deleted_draft() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id = create_group(&t, "a chat").await.unwrap();
let mut msg = Message::new_text("Foobar".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
@@ -823,15 +800,9 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
let chat_id1 = create_group(&t, "a chat").await.unwrap();
create_group(&t, "b chat").await.unwrap();
create_group(&t, "c chat").await.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();

View File

@@ -1,38 +1,39 @@
//! Implementation of Consistent Color Generation.
//! Color generation.
//!
//! Consistent Color Generation is defined in XEP-0392.
//!
//! Color Vision Deficiency correction is not implemented as Delta Chat does not offer
//! corresponding settings.
use hsluv::hsluv_to_rgb;
//! This is similar to Consistent Color Generation defined in XEP-0392,
//! but uses OKLCh colorspace instead of HSLuv
//! to ensure that colors have the same lightness.
use colorutils_rs::{Oklch, Rgb, TransferFunction};
use sha1::{Digest, Sha1};
/// Converts an identifier to Hue angle.
fn str_to_angle(s: &str) -> f64 {
fn str_to_angle(s: &str) -> f32 {
let bytes = s.as_bytes();
let result = Sha1::digest(bytes);
let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
f64::from(checksum) / 65536.0 * 360.0
f32::from(checksum) / 65536.0 * 360.0
}
/// Converts RGB tuple to a 24-bit number.
///
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
/// most significant bits corresponding to the red color.
fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
let r = ((r * 256.0) as u32).min(255);
let g = ((g * 256.0) as u32).min(255);
let b = ((b * 256.0) as u32).min(255);
65536 * r + 256 * g + b
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
}
/// Converts an identifier to RGB color.
///
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
/// half (50.0) to make colors suitable both for light and dark theme.
/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
pub fn str_to_color(s: &str) -> u32 {
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
let lightness = 0.5;
let chroma = 0.23;
let angle = str_to_angle(s);
let oklch = Oklch::new(lightness, chroma, angle);
let rgb = oklch.to_rgb(TransferFunction::Srgb);
rgb_to_u32(rgb)
}
/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
@@ -45,6 +46,7 @@ mod tests {
use super::*;
#[test]
#[allow(clippy::excessive_precision)]
fn test_str_to_angle() {
// Test against test vectors from
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
@@ -57,11 +59,11 @@ mod tests {
#[test]
fn test_rgb_to_u32() {
assert_eq!(rgb_to_u32((0.0, 0.0, 0.0)), 0);
assert_eq!(rgb_to_u32((1.0, 1.0, 1.0)), 0xffffff);
assert_eq!(rgb_to_u32((0.0, 0.0, 1.0)), 0x0000ff);
assert_eq!(rgb_to_u32((0.0, 1.0, 0.0)), 0x00ff00);
assert_eq!(rgb_to_u32((1.0, 0.0, 0.0)), 0xff0000);
assert_eq!(rgb_to_u32((1.0, 0.5, 0.0)), 0xff8000);
assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0)), 0);
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0xff, 0xff)), 0xffffff);
assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0xff)), 0x0000ff);
assert_eq!(rgb_to_u32(Rgb::new(0, 0xff, 0)), 0x00ff00);
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0, 0)), 0xff0000);
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0x80, 0)), 0xff8000);
}
}

View File

@@ -14,15 +14,15 @@ use tokio::fs;
use crate::blob::BlobObject;
use crate::configure::EnteredLoginParam;
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::{LogExt, info};
use crate::login_param::ConfiguredLoginParam;
use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{Provider, get_provider_by_id};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
use crate::transport::ConfiguredLoginParam;
use crate::{constants, stats};
/// The available configuration keys.
#[derive(
@@ -151,19 +151,11 @@ pub enum Config {
/// setting up a second device, or receiving a sync message.
BccSelf,
/// True if encryption is preferred according to Autocrypt standard.
#[strum(props(default = "1"))]
E2eeEnabled,
/// True if Message Delivery Notifications (read receipts) should
/// be sent and requested.
#[strum(props(default = "1"))]
MdnsEnabled,
/// True if "Sent" folder should be watched for changes.
#[strum(props(default = "0"))]
SentboxWatch,
/// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
/// ones are moved there anyway.
#[strum(props(default = "1"))]
@@ -289,9 +281,6 @@ pub enum Config {
/// Configured folder for chat messages.
ConfiguredMvboxFolder,
/// Configured "Sent" folder.
ConfiguredSentboxFolder,
/// Configured "Trash" folder.
ConfiguredTrashFolder,
@@ -350,9 +339,6 @@ pub enum Config {
/// Unset, when quota falls below minimal warning threshold again.
QuotaExceeding,
/// address to webrtc instance to use for videochats
WebrtcInstance,
/// Timestamp of the last time housekeeping was run
LastHousekeeping,
@@ -396,12 +382,6 @@ pub enum Config {
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Enable header protection for `Autocrypt` header.
///
/// This is an experimental setting not compatible to other MUAs
/// and older Delta Chat versions (core version <= v1.149.0).
ProtectAutocrypt,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]
@@ -417,23 +397,26 @@ pub enum Config {
#[strum(props(default = "172800"))]
GossipPeriod,
/// Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
/// and when the key changes, an info message is posted into the chat.
/// 0=Nothing else happens when the key changes.
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
/// until `chat_id.accept()` is called.
#[strum(props(default = "0"))]
VerifiedOneOnOneChats,
/// Row ID of the key in the `keypairs` table
/// used for signatures, encryption to self and included in `Autocrypt` header.
KeyId,
/// This key is sent to the self_reporting bot so that the bot can recognize the user
/// Send statistics to Delta Chat's developers.
/// Can be exposed to the user as a setting.
StatsSending,
/// Last time statistics were sent to Delta Chat's developers
StatsLastSent,
/// Last time `update_message_stats()` was called
StatsLastUpdate,
/// This key is sent to the statistics bot so that the bot can recognize the user
/// without storing the email address
SelfReportingId,
StatsId,
/// The last contact id that already existed when statistics-sending was enabled for the first time.
StatsLastOldContactId,
/// MsgId of webxdc map integration.
WebxdcIntegration,
@@ -454,6 +437,9 @@ pub enum Config {
/// to avoid encrypting it differently and
/// storing the same token multiple times on the server.
EncryptedDeviceToken,
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
FailOnReceivingFullMsg,
}
impl Config {
@@ -480,10 +466,7 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
)
matches!(self, Config::MvboxMove | Config::OnlyFetchMvbox)
}
}
@@ -596,8 +579,9 @@ impl Context {
/// Returns boolean configuration value for the given key.
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
Ok(self
.get_config_parsed::<i32>(key)
.get_config(key)
.await?
.and_then(|s| s.parse::<i32>().ok())
.map(|x| x != 0)
.unwrap_or_default())
}
@@ -609,15 +593,6 @@ impl Context {
|| !self.get_config_bool(Config::IsChatmail).await?)
}
/// Returns true if sentbox ("Sent" folder) should be watched.
pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SentboxWatch).await?
&& self
.get_config(Config::ConfiguredSentboxFolder)
.await?
.is_some())
}
/// Returns true if sync messages should be sent.
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SyncMsgs).await?
@@ -690,7 +665,7 @@ impl Context {
Config::Selfavatar if value.is_empty() => None,
Config::Selfavatar => {
config_value = BlobObject::store_from_base64(self, value)?;
Some(config_value.as_str())
config_value.as_deref()
}
_ => Some(value),
};
@@ -705,9 +680,7 @@ impl Context {
Config::Socks5Enabled
| Config::ProxyEnabled
| Config::BccSelf
| Config::E2eeEnabled
| Config::MdnsEnabled
| Config::SentboxWatch
| Config::MvboxMove
| Config::OnlyFetchMvbox
| Config::DeleteToTrash
@@ -734,12 +707,18 @@ impl Context {
Self::check_config(key, value)?;
let _pause = match key.needs_io_restart() {
true => self.scheduler.pause(self.clone()).await?,
true => self.scheduler.pause(self).await?,
_ => Default::default(),
};
if key == Config::StatsSending {
let old_value = self.get_config(key).await?;
let old_value = bool_from_config(old_value.as_deref());
let new_value = bool_from_config(value);
stats::pre_sending_config_change(self, old_value, new_value).await?;
}
self.set_config_internal(key, value).await?;
if key == Config::SentboxWatch {
self.last_full_folder_scan.lock().await.take();
if key == Config::StatsSending {
stats::maybe_send_stats(self).await?;
}
Ok(())
}
@@ -892,6 +871,10 @@ pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
Some(if val { "1" } else { "0" })
}
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
config.is_some_and(|v| v.parse::<i32>().unwrap_or_default() != 0)
}
// Separate impl block for self address handling
impl Context {
/// Determine whether the specified addr maps to the/a self addr.

View File

@@ -27,18 +27,21 @@ use crate::config::{self, Config};
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::{LogExt, info, warn};
use crate::log::{LogExt, warn};
use crate::login_param::EnteredCertificateChecks;
pub use crate::login_param::EnteredLoginParam;
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
};
use crate::message::Message;
use crate::net::proxy::ProxyConfig;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
use crate::smtp::Smtp;
use crate::sync::Sync::*;
use crate::tools::time;
use crate::transport::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate,
};
use crate::{EventType, stock_str};
use crate::{chat, provider};
use deltachat_contact_tools::addr_cmp;
@@ -117,7 +120,7 @@ impl Context {
Ok(())
}
async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
ensure!(
!self.scheduler.is_running().await,
"cannot configure, already running"
@@ -137,7 +140,7 @@ impl Context {
let res = self
.inner_configure(param)
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
.await;
self.free_ongoing().await;
@@ -162,20 +165,15 @@ impl Context {
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
self.stop_io().await;
// This code first sets the deprecated Config::Addr, Config::MailPw, etc.
// and then calls configure(), which loads them again.
// At some point, we will remove configure()
// and then simplify the code
// to directly create an EnteredLoginParam.
let result = async move {
match crate::qr::check_qr(self, qr).await? {
crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
let mut param = match crate::qr::check_qr(self, qr).await? {
crate::qr::Qr::Account { .. } => login_param_from_account_qr(self, qr).await?,
crate::qr::Qr::Login { address, options } => {
crate::qr::configure_from_login_qr(self, &address, options).await?
login_param_from_login_qr(&address, options)?
}
_ => bail!("QR code does not contain account"),
}
self.configure().await?;
};
self.add_transport_inner(&mut param).await?;
Ok(())
}
.await;
@@ -196,16 +194,11 @@ impl Context {
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
let transports = self
.sql
.query_map(
"SELECT entered_param FROM transports",
(),
|row| row.get::<_, String>(0),
|rows| {
rows.flatten()
.map(|s| Ok(serde_json::from_str(&s)?))
.collect::<Result<Vec<EnteredLoginParam>>>()
},
)
.query_map_vec("SELECT entered_param FROM transports", (), |row| {
let entered_param: String = row.get(0)?;
let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
Ok(transport)
})
.await?;
Ok(transports)
@@ -300,8 +293,6 @@ async fn get_configured_param(
param.smtp.password.clone()
};
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
let mut addr = param.addr.clone();
if param.oauth2 {
// the used oauth2 addr may differ, check this.
@@ -343,7 +334,7 @@ async fn get_configured_param(
"checking internal provider-info for offline autoconfig"
);
provider = provider::get_provider_info(ctx, &param_domain, proxy_enabled).await;
provider = provider::get_provider_info(&param_domain);
if let Some(provider) = provider {
if provider.server.is_empty() {
info!(ctx, "Offline autoconfig found, but no servers defined.");
@@ -555,7 +546,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
true => ctx.get_config_bool(Config::IsChatmail).await?,
};
if is_chatmail {
ctx.set_config(Config::SentboxWatch, None).await?;
ctx.set_config(Config::MvboxMove, Some("0")).await?;
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
ctx.set_config(Config::ShowEmails, None).await?;
@@ -575,14 +565,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
progress!(ctx, 910);
if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? {
if configured_addr != param.addr {
// Switched account, all server UIDs we know are invalid
info!(ctx, "Scheduling resync because the address has changed.");
ctx.schedule_resync().await?;
}
}
let provider = configured_param.provider;
configured_param
.save_to_transports_table(ctx, param)

View File

@@ -28,8 +28,9 @@ struct MozAutoconfigure {
pub outgoing_servers: Vec<Server>,
}
#[derive(Debug)]
#[derive(Debug, Default)]
enum MozConfigTag {
#[default]
Undefined,
Hostname,
Port,
@@ -37,12 +38,6 @@ enum MozConfigTag {
Username,
}
impl Default for MozConfigTag {
fn default() -> Self {
Self::Undefined
}
}
impl FromStr for MozConfigTag {
type Err = ();
@@ -106,7 +101,7 @@ fn parse_server<B: BufRead>(
}
}
Event::Text(ref event) => {
let val = event.unescape().unwrap_or_default().trim().to_owned();
let val = event.xml_content().unwrap_or_default().trim().to_owned();
match tag_config {
MozConfigTag::Hostname => hostname = Some(val),

View File

@@ -79,7 +79,7 @@ fn parse_protocol<B: BufRead>(
}
}
Event::Text(ref e) => {
let val = e.unescape().unwrap_or_default();
let val = e.xml_content().unwrap_or_default();
if let Some(ref tag) = current_tag {
match tag.as_str() {
@@ -123,7 +123,7 @@ fn parse_redirecturl<B: BufRead>(
let mut buf = Vec::new();
match reader.read_event_into(&mut buf)? {
Event::Text(ref e) => {
let val = e.unescape().unwrap_or_default();
let val = e.xml_content().unwrap_or_default();
Ok(val.trim().to_string())
}
_ => Ok("".to_string()),

View File

@@ -60,23 +60,6 @@ pub enum MediaQuality {
Worse = 1,
}
/// Video chat URL type.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(i8)]
pub enum VideochatType {
/// Unknown type.
#[default]
Unknown = 0,
/// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance.
BasicWebrtc = 1,
/// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance.
Jitsi = 2,
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
@@ -95,10 +78,11 @@ pub const DC_GCL_ADDRESS: u32 = 0x04;
pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// warn about an outdated app after a given number of days.
// as we use the "provider-db generation date" as reference (that might not be updated very often)
// and as not all system get speedy updates,
// reference is the release date.
// as not all system get speedy updates,
// do not use too small value that will annoy users checking for nonexistent updates.
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 365;
// "90 days" has proven to be too short at some point (user were informed but there was no update)
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 183;
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
@@ -117,6 +101,8 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
FromPrimitive,
ToPrimitive,
FromSql,
@@ -134,7 +120,7 @@ pub enum Chattype {
/// Group chat.
///
/// Created by [`crate::chat::create_group_chat`].
/// Created by [`crate::chat::create_group`].
Group = 120,
/// An (unencrypted) mailing list,
@@ -237,6 +223,9 @@ pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 5;
// `max_smtp_rcpt_to` in the provider db.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
/// Same as `DEFAULT_MAX_SMTP_RCPT_TO`, but for chatmail relays.
pub(crate) const DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO: usize = 999;
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours
@@ -264,6 +253,18 @@ pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
/// Period between `sql::housekeeping()` runs.
pub(crate) const HOUSEKEEPING_PERIOD: i64 = 24 * 60 * 60;
pub(crate) const BROADCAST_INCOMPATIBILITY_MSG: &str = r#"The up to now "experimental channels feature" is about to become an officially supported one. By that, privacy will be improved, it will become faster, and less traffic will be consumed.
As we do not guarantee feature-stability for such experiments, this means, that you will need to create the channel again.
Here is what to do:
• Create a new channel
• Tap on the channel name
• Tap on "QR Invite Code"
• Have all recipients scan the QR code, or send them the link
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
@@ -307,16 +308,4 @@ mod tests {
assert_eq!(MediaQuality::Balanced, MediaQuality::from_i32(0).unwrap());
assert_eq!(MediaQuality::Worse, MediaQuality::from_i32(1).unwrap());
}
#[test]
fn test_videochattype_values() {
// values may be written to disk and must not change
assert_eq!(VideochatType::Unknown, VideochatType::default());
assert_eq!(VideochatType::Unknown, VideochatType::from_i32(0).unwrap());
assert_eq!(
VideochatType::BasicWebrtc,
VideochatType::from_i32(1).unwrap()
);
assert_eq!(VideochatType::Jitsi, VideochatType::from_i32(2).unwrap());
}
}

View File

@@ -21,7 +21,7 @@ use tokio::task;
use tokio::time::{Duration, timeout};
use crate::blob::BlobObject;
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
use crate::chat::ChatId;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype};
@@ -31,12 +31,12 @@ use crate::key::{
DcKey, Fingerprint, SignedPublicKey, load_self_public_key, self_fingerprint,
self_fingerprint_opt,
};
use crate::log::{LogExt, info, warn};
use crate::log::{LogExt, warn};
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time, to_lowercase};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -369,16 +369,15 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
return Ok(id);
}
let path = match &contact.profile_image {
Some(image) => match BlobObject::store_from_base64(context, image) {
Err(e) => {
Some(image) => match BlobObject::store_from_base64(context, image)? {
None => {
warn!(
context,
"import_vcard_contact: Could not decode and save avatar for {}: {e:#}.",
contact.addr
"import_vcard_contact: Could not decode avatar for {}.", contact.addr
);
None
}
Ok(path) => Some(path),
Some(path) => Some(path),
},
None => None,
};
@@ -755,7 +754,19 @@ impl Contact {
self.is_bot
}
/// Check if an e-mail address belongs to a known and unblocked contact.
/// Looks up a known and unblocked contact with a given e-mail address.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
///
/// Known and unblocked contacts will be returned by `get_contacts()`.
///
@@ -795,14 +806,28 @@ impl Contact {
.query_get_value(
"SELECT id FROM contacts
WHERE addr=?1 COLLATE NOCASE
AND fingerprint='' -- Do not lookup key-contacts
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
AND id>?2 AND origin>=?3 AND (? OR blocked=?)
ORDER BY
(
SELECT COUNT(*) FROM chats c
INNER JOIN chats_contacts cc
ON c.id=cc.chat_id
WHERE c.type=?
AND c.id>?
AND c.blocked=?
AND cc.contact_id=contacts.id
) DESC,
last_seen DESC, fingerprint DESC
LIMIT 1",
(
&addr_normalized,
ContactId::LAST_SPECIAL,
min_origin as u32,
blocked.is_none(),
blocked.unwrap_or_default(),
blocked.unwrap_or(Blocked::Not),
Chattype::Single,
constants::DC_CHAT_ID_LAST_SPECIAL,
blocked.unwrap_or(Blocked::Not),
),
)
.await?;
@@ -1256,14 +1281,13 @@ impl Contact {
let list = context
.sql
.query_map(
.query_map_vec(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
(ContactId::LAST_SPECIAL,),
|row| row.get::<_, ContactId>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
|row| {
let contact_id: ContactId = row.get(0)?;
Ok(contact_id)
}
)
.await?;
Ok(list)
@@ -1538,7 +1562,7 @@ impl Contact {
return Ok(Some(chat::get_device_icon(context).await?));
}
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
return Ok(Some(chat::get_address_contact_icon(context).await?));
return Ok(Some(chat::get_unencrypted_icon(context).await?));
}
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
@@ -1548,12 +1572,23 @@ impl Contact {
Ok(None)
}
/// Get a color for the contact.
/// The color is calculated from the contact's email address
/// and can be used for an fallback avatar with white initials
/// as well as for headlines in bubbles of group chats.
/// Returns a color for the contact.
/// For self-contact this returns gray if own keypair doesn't exist yet.
/// See also [`self::get_color`].
pub fn get_color(&self) -> u32 {
str_to_color(&self.addr.to_lowercase())
get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint())
}
/// Returns a color for the contact.
/// Ensures that the color isn't gray. For self-contact this generates own keypair if it doesn't
/// exist yet.
/// See also [`self::get_color`].
pub async fn get_or_gen_color(&self, context: &Context) -> Result<u32> {
let mut fpr = self.fingerprint();
if fpr.is_none() && self.id == ContactId::SELF {
fpr = Some(load_self_public_key(context).await?.dc_fingerprint());
}
Ok(get_color(self.id == ContactId::SELF, &self.addr, &fpr))
}
/// Gets the contact's status.
@@ -1619,29 +1654,6 @@ impl Contact {
}
}
/// Returns if the contact profile title should display a green checkmark.
///
/// This generally should be consistent with the 1:1 chat with the contact
/// so 1:1 chat with the contact and the contact profile
/// either both display the green checkmark or both don't display a green checkmark.
///
/// UI often knows beforehand if a chat exists and can also call
/// `chat.is_protected()` (if there is a chat)
/// or `contact.is_verified()` (if there is no chat) directly.
/// This is often easier and also skips some database calls.
pub async fn is_profile_verified(&self, context: &Context) -> Result<bool> {
let contact_id = self.id;
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, contact_id).await?
{
Ok(chat_id.is_protected(context).await? == ProtectionStatus::Protected)
} else {
// 1:1 chat does not exist.
Ok(self.is_verified(context).await?)
}
}
/// Returns the number of real (i.e. non-special) contacts in the database.
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
if !context.sql.is_open().await {
@@ -1672,6 +1684,21 @@ impl Contact {
}
}
/// Returns a color for a contact having given attributes.
///
/// The color is calculated from contact's fingerprint (for key-contacts) or email address (for
/// address-contacts; should be lowercased to avoid allocation) and can be used for an fallback
/// avatar with white initials as well as for headlines in bubbles of group chats.
pub fn get_color(is_self: bool, addr: &str, fingerprint: &Option<Fingerprint>) -> u32 {
if let Some(fingerprint) = fingerprint {
str_to_color(&fingerprint.hex())
} else if is_self {
0x808080
} else {
str_to_color(&to_lowercase(addr))
}
}
// Updates the names of the chats which use the contact name.
//
// This is one of the few duplicated data, however, getting the chat list is easier this way.
@@ -1734,8 +1761,7 @@ pub(crate) async fn set_blocked(
) -> Result<()> {
ensure!(
!contact_id.is_special(),
"Can't block special contact {}",
contact_id
"Can't block special contact {contact_id}"
);
let contact = Contact::get_by_id(context, contact_id).await?;
@@ -1775,9 +1801,7 @@ WHERE type=? AND id IN (
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Some((chat_id, _, _)) =
chat::get_chat_id_by_grpid(context, &contact.addr).await?
{
if let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? {
chat_id.unblock_ex(context, Nosync).await?;
}
}
@@ -1917,16 +1941,21 @@ pub(crate) async fn update_last_seen(
}
/// Marks contact `contact_id` as verified by `verifier_id`.
///
/// `verifier_id == None` means that the verifier is unknown.
pub(crate) async fn mark_contact_id_as_verified(
context: &Context,
contact_id: ContactId,
verifier_id: ContactId,
verifier_id: Option<ContactId>,
) -> Result<()> {
ensure_and_debug_assert_ne!(contact_id, ContactId::SELF,);
ensure_and_debug_assert_ne!(
contact_id,
Some(contact_id),
verifier_id,
"Contact cannot be verified by self",
);
let by_self = verifier_id == Some(ContactId::SELF);
let mut verifier_id = verifier_id.unwrap_or(contact_id);
context
.sql
.transaction(|transaction| {
@@ -1939,20 +1968,33 @@ pub(crate) async fn mark_contact_id_as_verified(
bail!("Non-key-contact {contact_id} cannot be verified");
}
if verifier_id != ContactId::SELF {
let verifier_fingerprint: String = transaction.query_row(
"SELECT fingerprint FROM contacts WHERE id=?",
(verifier_id,),
|row| row.get(0),
)?;
let (verifier_fingerprint, verifier_verifier_id): (String, ContactId) = transaction
.query_row(
"SELECT fingerprint, verifier FROM contacts WHERE id=?",
(verifier_id,),
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
if verifier_fingerprint.is_empty() {
bail!(
"Contact {contact_id} cannot be verified by non-key-contact {verifier_id}"
);
}
ensure!(
verifier_id == contact_id || verifier_verifier_id != ContactId::UNDEFINED,
"Contact {contact_id} cannot be verified by unverified contact {verifier_id}",
);
if verifier_verifier_id == verifier_id {
// Avoid introducing incorrect reverse chains: if the verifier itself has an
// unknown verifier, it may be `contact_id` actually (directly or indirectly) on
// the other device (which is needed for getting "verified by unknown contact"
// in the first place).
verifier_id = contact_id;
}
}
transaction.execute(
"UPDATE contacts SET verifier=? WHERE id=?",
(verifier_id, contact_id),
"UPDATE contacts SET verifier=?1
WHERE id=?2 AND (verifier=0 OR verifier=id OR ?3)",
(verifier_id, contact_id, by_self),
)?;
Ok(())
})
@@ -2017,7 +2059,7 @@ impl RecentlySeenLoop {
// become unseen in the future.
let mut unseen_queue: BinaryHeap<MyHeapElem> = context
.sql
.query_map(
.query_map_collect(
"SELECT id, last_seen FROM contacts
WHERE last_seen > ?",
(now_ts - SEEN_RECENTLY_SECONDS,),
@@ -2026,10 +2068,6 @@ impl RecentlySeenLoop {
let last_seen: i64 = row.get("last_seen")?;
Ok((Reverse(last_seen + SEEN_RECENTLY_SECONDS), contact_id))
},
|rows| {
rows.collect::<std::result::Result<BinaryHeap<MyHeapElem>, _>>()
.map_err(Into::into)
},
)
.await
.unwrap_or_default();

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