Compare commits

..

88 Commits

Author SHA1 Message Date
link2xt
3bb6b89f1b Fingerprint cannot be the primary key 2023-11-10 06:37:16 +00:00
link2xt
bb5b97f168 docs: add a draft of a proposed simplified acpeerstates table 2023-11-09 23:43:01 +00:00
link2xt
1856c622a1 test(deltachat-rpc-client): log all events as debug messages
This is necessary to debug the tests.
2023-11-09 10:06:05 +01:00
link2xt
0a48a2effa refactor: replace inviter_progress macro with a function 2023-11-09 01:58:25 +00:00
link2xt
543864f0f5 test(deltachat-rpc-client): fix securejoin tests
Wait for Bob (joiner) progress 1000
and do not return too early in test_qr_setup_contact().
2023-11-09 01:57:13 +00:00
link2xt
0fe94e47cc test: enable verified 1:1 chats in deltachat-rpc-client tests 2023-11-09 01:57:13 +00:00
link2xt
fc09210aea api: emit JoinerProgress(1000) event when Bob verifies Alice 2023-11-09 01:57:13 +00:00
Hocuri
3e194969c0 fix: Mark 1:1 chat as protected when joining a group 2023-11-09 01:57:13 +00:00
link2xt
391cffb454 docs: add dc_chat_is_protection_broken() to 1.127.0 changelog 2023-11-08 17:53:17 +00:00
Sebastian Klähn
47486f8bab Add Changelog entry for is_protection_broken (#4959)
close #4885
2023-11-08 18:41:55 +01:00
link2xt
620e363ce6 refactor(deltachat-rpc-client): use itertools for thread-safe request ID generation 2023-11-08 17:06:25 +00:00
link2xt
0c2276775d test: test that joining a group verifies Alice and vice versa 2023-11-08 01:56:45 +00:00
iequidoo
ad51a7cd85 feat: apply_group_changes: Add system messages about implicitly added members 2023-11-07 21:09:25 -03:00
B. Petersen
28952789a4 fix: raise lower auto-download limit to 160k 2023-11-07 23:27:02 +00:00
link2xt
14adcdb517 fix: treat reset state as encryption not preferred
This will still degrade 1:1 chats to no encryption,
but will not cause the group to disable encryption
simply because one user got into reset state.
2023-11-07 21:24:23 +00:00
link2xt
cc80590488 test(deltachat-rpc-client): move securejoin tests to a separate file 2023-11-07 18:21:44 +00:00
link2xt
48416289ac api: add dc_contact_is_profile_verified() 2023-11-07 18:14:33 +00:00
link2xt
003a27f625 refactor: hide ChatId::get_for_contact() from public API 2023-11-07 01:19:14 +00:00
bjoern
bff4a2259f chore: update provider-db (#4949) 2023-11-07 01:12:31 +00:00
link2xt
9adf856705 chore: upgrade toml dependency 2023-11-06 22:22:12 +00:00
link2xt
2215de5285 chore(release): prepare for 1.129.1 2023-11-06 20:05:13 +00:00
iequidoo
013467d6c6 test: Group chats device synchronisation 2023-11-06 20:02:28 +00:00
Sebastian Klähn
1f52b8af2f fix: Partial messages do not change group state (#4900)
This message makes that partial messages do not change the group state.
A simple fix and a comprehensive test is added. This is a follow up to
the former #4841 which took a different approach.
2023-11-06 20:01:55 +00:00
iequidoo
ce32f76265 fix: apply_group_changes: Don't implicitly delete members locally, add absent ones instead (#4934)
This is another approach to provide group membership consistency for all members. Considerations:
- Classical MUA users usually don't intend to remove users from an email thread, so if they removed
  a recipient then it was probably by accident.
- DC users could miss new member additions and then better to handle this in the same way as for
  classical MUA messages. Moreover, if we remove a member implicitly, they will never know that and
  continue to think they're still here.

But it shouldn't be a big problem if somebody missed a member removal, because they will likely
recreate the member list from the next received message. The problem occurs only if that "somebody"
managed to reply earlier. Really, it's a problem for big groups with high message rate, but let it
be for now.
2023-11-06 20:00:54 +00:00
link2xt
836f65376c fix(deltachat-rpc-client): add the Lock around request ID
Avoid handing out the same request ID twice.
2023-11-06 20:00:17 +00:00
link2xt
99940dd28c fix: update tokio-imap to fix Outlook STATUS parsing bug 2023-11-06 19:05:36 +00:00
link2xt
ffeb801b58 chore(release): prepare for 1.129.0 2023-11-06 11:07:03 +00:00
link2xt
339bbcf070 chore: update dependencies 2023-11-06 10:53:40 +00:00
link2xt
7b83bddc2d fix(imap): always advance expected UIDNEXT to avoid skipping IDLE in a loop
Ensure the client does not busy loop
skipping IDLE if UIDNEXT of the mailbox is higher than
the last seen UID plus 1, e.g. if the message
with UID=UIDNEXT-1 was deleted before we fetched it.

We do not fallback to UIDNEXT=1 anymore
if the STATUS command cannot determine UIDNEXT.
There are no known IMAP servers with broken STATUS so far.
This allows to guarantee that select_with_uidvalidity()
sets UIDNEXT for the mailbox and use it in fetch_new_messages()
to ensure that UIDNEXT always advances even
when there are no messages to fetch.
2023-11-06 10:30:25 +00:00
link2xt
5549733a0b test: fix flaky test_forward_increation() test 2023-11-06 04:46:10 +00:00
link2xt
4e21917c0e feat: enable sync messages by default 2023-11-05 21:11:47 +00:00
bjoern
939b4b2aab feat: add 'group created instructions' as info message (#4916)
Before, it was shown by UI when the chat is empty, however, in case of
verified groups, this is never the case any longer as the 'e2ee now
guaranteed' is always added.
2023-11-05 19:24:34 +00:00
link2xt
fd0770859d chore(deltachat-jsonrpc): remove unused node-fetch dependency 2023-11-05 18:19:18 +00:00
link2xt
d5854fb3c9 chore(node): update @types/node 2023-11-05 18:16:37 +00:00
link2xt
3aa22a27cc chore(node): remove unused node-fetch dependency 2023-11-05 18:14:38 +00:00
link2xt
10b1a2f5f5 chore(node): update prettier dependency 2023-11-05 18:12:16 +00:00
link2xt
a28a34773c docs: remove documentation for non-existing dc_accounts_new os_name param 2023-11-05 17:54:00 +00:00
link2xt
7c744d14d7 docs: contact profile view should not use dc_contact_is_verified()
Green checkmark in the contact profile
should only be displayed in the title
if the same checkmark is displayed in the title of 1:1 chat.
If 1:1 chat does not exist,
the checkmark should not be displayed in the title
of the contact profile.

Also add docs to is_protected
property of FullChat and BasicChat JSON objects.
2023-11-05 17:21:23 +00:00
link2xt
6c68f2eb7e chore: update dependencies 2023-11-05 15:54:29 +00:00
link2xt
eb2d2b7313 refactor: accept &str instead of Option<String> in idle() 2023-11-05 15:32:37 +00:00
link2xt
2e50abedaa fix: check UIDNEXT with a STATUS command before going IDLE
This prevents accidentally going IDLE
when the last new message has arrived
while the folder was closed.
For example, this happened in some tests:
1. INBOX is selected to fetch, move and delete messages.
2. One of the messages is deleted.
3. INBOX is closed to expunge the message.
4. A new message arrives.
5. INBOX is selected with (CONDSTORE) to sync flags.
6. Delta Chat goes into IDLE without downloading the new message.

To determine that a new message has arrived
we need to notice that UIDNEXT has advanced when selecting the folder.
However, some servers such as Winmail Pro Mail Server 5.1.0616
do not return UIDNEXT in response to SELECT command.

To avoid interdependencies with the code
SELECTing the folder and having to implement
STATUS fallback after each SELECT even when we
may not want to go IDLE due to interrupt or unsolicited EXISTS,
we simply call STATUS unconditionally before IDLE.
2023-11-05 09:58:58 +01:00
link2xt
ee53136ed2 refactor: add hostname to "no DNS resolution results" error message 2023-11-05 09:58:33 +01:00
iequidoo
e923983dca fix: Synchronise self-chat
It just didn't work before, sync messages were not generated.
2023-11-04 19:16:35 -03:00
iequidoo
f4753862f1 feat: Sync chat state immediately (#4817)
Sync messages are only sent on explicit user actions and only one per action, so it's safe to send
them right away not worrying about the rate limit on the server.
2023-11-04 19:16:35 -03:00
link2xt
16b40f3a19 feat: hardcode mail.sangham.net into DNS cache 2023-11-04 17:48:57 +00:00
iequidoo
9cd3a7550b fix: Switch to EncryptionPreference::Mutual on a receipt of encrypted+signed message (#4707) 2023-11-03 23:32:02 -03:00
link2xt
d840a7e6b9 api!: remove deprecated get_verifier_addr 2023-11-03 23:17:09 +00:00
link2xt
d875691955 chore(cargo): update dependencies 2023-11-03 21:12:10 +00:00
bjoern
c600bfa8ca docs: refine Contact::get_verifier_id and Contact::is_verified documentation (#4922)
Co-authored-by: link2xt <link2xt@testrun.org>
2023-11-03 21:11:03 +00:00
link2xt
973ffa1a64 fix: allow to change verified key via "member added" message
"Member added" message likely happens because
the contact adding a new member has another
chat with the contact
2023-11-03 17:51:11 +00:00
link2xt
caffc3d93c fix(json-rpc): return verifier even if the contact is not verified
This may happen if autocrypt key is changed after verification.
UI should still display who introduced the contact,
especially if the contact is still a member of some verified group.
If the contact is at the same time not verified,
i.e. its autocrypt key does not match verified key,
UI may display a crossed out checkmark instead of green checkmark.
2023-11-03 17:51:11 +00:00
link2xt
a4dcf656f3 api: add JSON-RPC get_chat_id_by_contact_id API (#4918) 2023-11-03 13:55:07 +00:00
link2xt
7bb5d48966 refactor: improve error handling in securejoin code
Result::Err is reserved for local errors,
such as database failures.
Not found peerstate in the database is a protocol failure,
so just return Ok(false) in mark_peer_as_verified().

This allows to handle more errors with `?`.
2023-11-02 15:54:35 +00:00
holger krekel
71b7b0b393 fix doc strings for qr-code joinings 2023-11-02 13:31:35 +01:00
holger krekel
bd02eea66b refactor: remove unused or useless code paths in securejoin (#4897) 2023-11-02 12:29:48 +00:00
iequidoo
cdcb10fb58 refactor: Make SyncData::AlterChat an inline struct 2023-11-02 08:47:20 -03:00
iequidoo
6cd7296001 refactor: Replace Context::nosync flag with internal functions taking enum Sync (#4817) 2023-11-02 08:47:20 -03:00
iequidoo
168021523f feat: Sync Contact::blocked across devices (#4817) 2023-11-02 08:47:20 -03:00
iequidoo
03f2635296 feat: Sync chat mute_duration across devices (#4817) 2023-11-02 08:47:20 -03:00
iequidoo
e3b08fa92b feat: Sync chat visibility across devices (#4817) 2023-11-02 08:47:20 -03:00
iequidoo
79cebe66de feat: Sync chat Blocked state across devices (#4817) 2023-11-02 08:47:20 -03:00
link2xt
0619e2a129 chore(release): prepare for 1.128.0 2023-11-02 01:53:42 +00:00
Simon Laux
64a81e4f61 nodejs: update and fix typedoc 2023-11-02 01:27:08 +00:00
link2xt
0431ae53ca hack: decrease ratelimit for .testrun.org subdomains 2023-11-02 00:30:44 +00:00
Simon Laux
89c873acd0 changed!: upgrade nodejs version to 18 (#4903) 2023-11-02 01:23:48 +01:00
B. Petersen
2e70cf9388 remove comment pointing to nothing
documentation and reasoning is fully in `delete_chat()`,
the remove line does not add much to it, also, the hint to "above" is wrong.
2023-11-01 15:46:44 +01:00
link2xt
2efd0461d1 Revert "fix: add secondary verified key"
This reverts commit 5efb100f12.
2023-11-01 13:55:39 +00:00
holger krekel
196a34684d simplify "broken bobstate" as the state still passes 2023-11-01 00:58:54 +01:00
holger krekel
402fd6850c do the actual fix (thanks alex for some help) 2023-11-01 00:58:54 +01:00
holger krekel
72515f440d write a failing test 2023-11-01 00:58:54 +01:00
link2xt
045d919cdc refactor(deltachat-rpc-client): factor out resetup_account() 2023-10-31 23:08:57 +00:00
link2xt
9b9108320e refactor: avoid loading peerstate if there is no Autocrypt-Gossip 2023-10-31 23:08:57 +00:00
link2xt
5efb100f12 fix: add secondary verified key 2023-10-31 23:08:57 +00:00
link2xt
b747dd6ae8 refactor: remove unused argument from set_verified()
It is always PeerstateVerifiedStatus::BidirectVerified
and is always passed as a constant.
2023-10-31 23:08:57 +00:00
link2xt
ed2bc9e44d fix: remove previous attempt to recover from verified key change
This approach was introduced in the C core before Rust conversion:
<ced88321eb>
It does not have tests and does not practically help,
so we remove it in favor of alternative discussed in
<https://github.com/deltachat/deltachat-core-rust/issues/4541>
2023-10-31 23:08:57 +00:00
link2xt
9e1a2149fa test: test recovery of verified group via gossip 2023-10-31 23:08:57 +00:00
link2xt
22b6d8c17b feat(deltachat-rpc-client): add Account.wait_for_incoming_msg_event() 2023-10-31 23:08:57 +00:00
link2xt
3876846410 refactor: sort member vector before deduplicating
Otherwise SELF contact in the beginning of the vector
and in to_ids may be repeated twice and not deduplicated.
dedup() only deduplicates consecutive elements.
2023-10-31 23:08:57 +00:00
link2xt
a93c79e001 fix: allow other verified group recipients to be unverified
We may not have a verified key for other members
because we lost a gossip message.
Still, if the message is signed with a verified key
of the sender, there is no reason to replace it with an error.
2023-10-31 23:08:57 +00:00
link2xt
6aae0276da test: use instant accounts instead of mailadm 2023-10-30 00:15:22 +00:00
link2xt
1d80659bc3 chore: update portable-atomic dependency
Version 1.5.0 is yanked.
2023-10-29 23:36:26 +00:00
link2xt
94d5e86d4f refactor: rename repl_msg_by_error into replace_msg_by_error
This function has been named like this since it was a C function.
`repl` is unclear because it may stand for `reply`
as well as `replace`.
2023-10-29 17:09:59 +00:00
link2xt
aecf7729d8 chore(release): prepare for 1.127.2 2023-10-29 16:29:04 +00:00
Simon Laux
f130d537b7 api(jsonrpc): add get_message_info_object 2023-10-29 16:26:01 +00:00
Ajay Gonepuri
f30f862e7e docs: fix typos 2023-10-29 13:13:44 +00:00
link2xt
81e1164358 test: compile deltachat-rpc-server in debug mode for tests
This reduces compilation times.
2023-10-29 01:09:35 +00:00
Simon Laux
542bd4cbb8 api!: jsonrpc misc_set_draft now requires setting the viewtype 2023-10-28 08:42:14 +00:00
link2xt
771b57778e test: increase pytest timeout to 10 minutes
deltachat-rpc-client tests often timeouts on CI,
this hopefully fixes the problem unless the tests actually deadlock.
2023-10-28 00:54:36 +00:00
86 changed files with 2645 additions and 940 deletions

View File

@@ -211,7 +211,7 @@ jobs:
- name: Run python tests
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
@@ -274,6 +274,6 @@ jobs:
- name: Run deltachat-rpc-client tests
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
working-directory: deltachat-rpc-client
run: tox -e py

View File

@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "18"
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1

View File

@@ -15,10 +15,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x
- name: Add Rust cache
uses: Swatinem/rust-cache@v2
- name: npm install
@@ -31,7 +31,7 @@ jobs:
working-directory: deltachat-jsonrpc/typescript
run: npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
- name: make sure websocket server version still builds
working-directory: deltachat-jsonrpc
run: cargo build --bin deltachat-jsonrpc-server --features webserver

View File

@@ -16,10 +16,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x
- name: npm install and generate documentation
working-directory: node

View File

@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "18"
- name: System info
run: |
rustc -vV
@@ -66,10 +66,8 @@ jobs:
runs-on: ubuntu-latest
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
container: ubuntu:18.04
# Debian 10 contained glibc 2.28: https://packages.debian.org/buster/libc6
container: debian:10
steps:
# Working directory is owned by 1001:1001 by default.
# Change it to our user.
@@ -80,7 +78,7 @@ jobs:
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "18"
- run: apt-get update
# Python is needed for node-gyp
@@ -143,7 +141,7 @@ jobs:
uses: actions/checkout@v3
- uses: actions/setup-node@v2
with:
node-version: "16"
node-version: "18"
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1

View File

@@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "18"
- name: System info
run: |
rustc -vV
@@ -63,5 +63,5 @@ jobs:
working-directory: node
run: npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"

View File

@@ -1,5 +1,81 @@
# Changelog
## [1.129.1] - 2023-11-06
### Fixes
- Update tokio-imap to fix Outlook STATUS parsing bug.
- deltachat-rpc-client: Add the Lock around request ID.
- `apply_group_changes`: Don't implicitly delete members locally, add absent ones instead ([#4934](https://github.com/deltachat/deltachat-core-rust/pull/4934)).
- Partial messages do not change group state ([#4900](https://github.com/deltachat/deltachat-core-rust/pull/4900)).
### Tests
- Group chats device synchronisation.
## [1.129.0] - 2023-11-06
### API-Changes
- Add JSON-RPC `get_chat_id_by_contact_id` API ([#4918](https://github.com/deltachat/deltachat-core-rust/pull/4918)).
- [**breaking**] Remove deprecated `get_verifier_addr`.
### Features / Changes
- Sync chat `Blocked` state, chat visibility, chat mute duration and contact blocked status across devices ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
- Add 'group created instructions' as info message ([#4916](https://github.com/deltachat/deltachat-core-rust/pull/4916)).
- Add hardcoded fallback DNS cache.
### Fixes
- Switch to `EncryptionPreference::Mutual` on a receipt of encrypted+signed message ([#4707](https://github.com/deltachat/deltachat-core-rust/pull/4707)).
- imap: Check UIDNEXT with a STATUS command before going IDLE.
- Allow to change verified key via "member added" message.
- json-rpc: Return verifier even if the contact is not "verified" (Autocrypt key does not equal Secure-Join key).
### Documentation
- Refine `Contact::get_verifier_id` and `Contact::is_verified` documentation ([#4922](https://github.com/deltachat/deltachat-core-rust/pull/4922)).
- Contact profile view should not use `dc_contact_is_verified()`.
- Remove documentation for non-existing `dc_accounts_new` `os_name` param.
### Refactor
- Remove unused or useless code paths in Secure-Join ([#4897](https://github.com/deltachat/deltachat-core-rust/pull/4897)).
- Improve error handling in Secure-Join code.
- Add hostname to "no DNS resolution results" error message.
- Accept `&str` instead of `Option<String>` in idle().
## [1.128.0] - 2023-11-02
### Build system
- [**breaking**] Upgrade nodejs version to 18 ([#4903](https://github.com/deltachat/deltachat-core-rust/pull/4903)).
### Features / Changes
- deltachat-rpc-client: Add `Account.wait_for_incoming_msg_event()`.
- Decrease ratelimit for .testrun.org subdomains.
### Fixes
- Do not fail securejoin due to unrelated pending bobstate ([#4896](https://github.com/deltachat/deltachat-core-rust/pull/4896)).
- Allow other verified group recipients to be unverified, only check the sender verification.
- Remove not working attempt to recover from verified key changes.
## [1.127.2] - 2023-10-29
### API-Changes
- [**breaking**] Jsonrpc `misc_set_draft` now requires setting the viewtype.
- jsonrpc: Add `get_message_info_object`.
### Tests
- deltachat-rpc-client: Move pytest option from pyproject.toml to tox.ini and set log level.
- deltachat-rpc-client: Test securejoin.
- Increase pytest timeout to 10 minutes.
- Compile deltachat-rpc-server in debug mode for tests.
## [1.127.1] - 2023-10-27
### API-Changes
@@ -22,6 +98,8 @@
- [**breaking**] Remove unused `dc_set_chat_protection()`
- Hide `DcSecretKey` trait from the API.
- Verified 1:1 chats ([#4315](https://github.com/deltachat/deltachat-core-rust/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config.
- Add api `chat::Chat::is_protection_broken`
- Add `dc_chat_is_protection_broken()` C API.
### CI
@@ -1078,7 +1156,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
### Changes
- Look at Authentication-Results. Don't accept Autocrypt key changes
if they come with negative authentiation results while this contact
if they come with negative authentication results while this contact
sent emails with positive authentication results in the past. #3583
- jsonrpc in cffi also sends events now #3662
- jsonrpc: new format for events and better typescript autocompletion
@@ -2664,7 +2742,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
- delete all consumed secure-join handshake messagess #1209 #1212
- rust-level cleanups #1218 #1217 #1210 #1205
- Rust-level cleanups #1218 #1217 #1210 #1205
- python-level cleanups #1204 #1202 #1201
@@ -3034,3 +3112,7 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.126.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.0...v1.126.1
[1.127.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.1...v1.127.0
[1.127.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.0...v1.127.1
[1.127.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.1...v1.127.2
[1.128.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.2...v1.128.0
[1.129.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.128.0...v1.129.0
[1.129.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.0...v1.129.1

319
Cargo.lock generated
View File

@@ -198,12 +198,12 @@ dependencies = [
[[package]]
name = "async-channel"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336d835910fab747186c56586562cb46f42809c2843ef3a84f47509009522838"
checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e"
dependencies = [
"concurrent-queue",
"event-listener 3.0.0",
"event-listener 3.0.1",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
@@ -224,11 +224,11 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936c1b580be4373b48c9c687e0c79285441664398354df28d0860087cac0c069"
checksum = "2e542b1682eba6b85a721daef0c58e79319ffd0c678565c07ac57c8071c548b5"
dependencies = [
"async-channel 1.9.0",
"async-channel 2.1.0",
"base64 0.21.5",
"bytes",
"chrono",
@@ -290,7 +290,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -512,9 +512,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "2.5.0"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -824,9 +824,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4"
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
dependencies = [
"libc",
]
@@ -983,13 +983,13 @@ dependencies = [
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -1087,11 +1087,11 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.127.1"
version = "1.129.1"
dependencies = [
"ansi_term",
"anyhow",
"async-channel 2.0.0",
"async-channel 2.1.0",
"async-imap",
"async-native-tls",
"async-smtp",
@@ -1165,10 +1165,10 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.127.1"
version = "1.129.1"
dependencies = [
"anyhow",
"async-channel 2.0.0",
"async-channel 2.1.0",
"axum",
"base64 0.21.5",
"deltachat",
@@ -1189,7 +1189,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.127.1"
version = "1.129.1"
dependencies = [
"ansi_term",
"anyhow",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.127.1"
version = "1.129.1"
dependencies = [
"anyhow",
"deltachat",
@@ -1224,12 +1224,12 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
name = "deltachat_ffi"
version = "1.127.1"
version = "1.129.1"
dependencies = [
"anyhow",
"deltachat",
@@ -1433,7 +1433,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -1461,9 +1461,9 @@ dependencies = [
[[package]]
name = "dyn-clone"
version = "1.0.14"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd"
checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
[[package]]
name = "ecdsa"
@@ -1612,6 +1612,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "embedded-io"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "encoded-words"
version = "0.2.0"
@@ -1720,7 +1726,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -1733,7 +1739,7 @@ dependencies = [
"num-traits",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -1792,9 +1798,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "3.0.0"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325"
checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1"
dependencies = [
"concurrent-queue",
"parking",
@@ -1807,7 +1813,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160"
dependencies = [
"event-listener 3.0.0",
"event-listener 3.0.1",
"pin-project-lite",
]
@@ -1864,9 +1870,9 @@ dependencies = [
[[package]]
name = "fdeflate"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10"
checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868"
dependencies = [
"simd-adler32",
]
@@ -1893,9 +1899,9 @@ dependencies = [
[[package]]
name = "fiat-crypto"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d"
checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
[[package]]
name = "filetime"
@@ -1968,9 +1974,9 @@ version = "1.0.0"
[[package]]
name = "futures"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
dependencies = [
"futures-channel",
"futures-core",
@@ -1983,9 +1989,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
dependencies = [
"futures-core",
"futures-sink",
@@ -1993,15 +1999,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
[[package]]
name = "futures-executor"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
dependencies = [
"futures-core",
"futures-task",
@@ -2010,15 +2016,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
[[package]]
name = "futures-lite"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c1155db57329dca6d018b61e76b1488ce9a2e5e44028cac420a5898f4fcef63"
checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb"
dependencies = [
"fastrand",
"futures-core",
@@ -2026,37 +2032,36 @@ dependencies = [
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "futures-macro"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
name = "futures-sink"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
[[package]]
name = "futures-task"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
[[package]]
name = "futures-util"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
dependencies = [
"futures-channel",
"futures-core",
@@ -2330,9 +2335,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "human-panic"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b82da652938b83f94cfdaaf9ae7aaadb8430d84b0dfda226998416318727eac2"
checksum = "7a79a67745be0cb8dd2771f03b24c2f25df98d5471fe7a595d668cfa2e6f843d"
dependencies = [
"backtrace",
"os_info",
@@ -2460,9 +2465,9 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.16.2"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73b1b63179418b20aa81002d616c5f21b4ba257da9bca6989ea64dc573933e0"
checksum = "305c25c6e69416059e3396c4a062b84dc7b0a782cd4c84d82bab268eb0421ec7"
dependencies = [
"nom",
]
@@ -2479,9 +2484,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.0.2"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [
"equivalent",
"hashbrown 0.14.2",
@@ -2591,9 +2596,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
[[package]]
name = "js-sys"
version = "0.3.64"
version = "0.3.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
dependencies = [
"wasm-bindgen",
]
@@ -2651,9 +2656,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.149"
version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "libm"
@@ -2661,6 +2666,17 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libredox"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
dependencies = [
"bitflags 2.4.1",
"libc",
"redox_syscall 0.4.1",
]
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
@@ -2974,7 +2990,7 @@ checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -3067,9 +3083,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.57"
version = "0.10.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
@@ -3088,7 +3104,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -3108,9 +3124,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.93"
version = "0.9.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9"
dependencies = [
"cc",
"libc",
@@ -3206,13 +3222,13 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.8"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.3.5",
"redox_syscall 0.4.1",
"smallvec",
"windows-targets",
]
@@ -3296,7 +3312,7 @@ dependencies = [
"p384 0.13.0",
"rand 0.8.5",
"ripemd",
"rsa 0.9.2",
"rsa 0.9.3",
"sha1",
"sha2 0.10.8",
"sha3",
@@ -3325,7 +3341,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -3391,9 +3407,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "platforms"
version = "3.1.2"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8"
checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0"
[[package]]
name = "plotters"
@@ -3438,18 +3454,19 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b559898e0b4931ed2d3b959ab0c2da4d99cc644c4b0b1a35b4d344027f474023"
checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b"
[[package]]
name = "postcard"
version = "1.0.6"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb"
checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8"
dependencies = [
"cobs",
"const_format",
"embedded-io",
"postcard-derive",
"serde",
]
@@ -3499,9 +3516,9 @@ dependencies = [
[[package]]
name = "primeorder"
version = "0.13.2"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3"
checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4"
dependencies = [
"elliptic-curve 0.13.6",
]
@@ -3784,15 +3801,6 @@ dependencies = [
"yasna",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
@@ -3803,13 +3811,22 @@ dependencies = [
]
[[package]]
name = "redox_users"
version = "0.4.3"
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_users"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
dependencies = [
"getrandom 0.2.10",
"redox_syscall 0.2.16",
"libredox",
"thiserror",
]
@@ -3993,16 +4010,14 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8"
checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d"
dependencies = [
"byteorder",
"const-oid",
"digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-iter",
"num-traits",
"pkcs1 0.7.5",
"pkcs8 0.10.2",
@@ -4065,9 +4080,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.20"
version = "0.38.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
dependencies = [
"bitflags 2.4.1",
"errno",
@@ -4295,9 +4310,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.190"
version = "1.0.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
checksum = "a834c4821019838224821468552240d4d95d14e751986442c816572d39a080c9"
dependencies = [
"serde_derive",
]
@@ -4322,13 +4337,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.190"
version = "1.0.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
checksum = "46fa52d5646bce91b680189fe5b1c049d2ea38dabb4e2e7c8d00ca12cfbfbcfd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -4344,9 +4359,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.107"
version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
"itoa",
"ryu",
@@ -4629,7 +4644,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -4651,9 +4666,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.38"
version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"proc-macro2",
"quote",
@@ -4721,13 +4736,13 @@ checksum = "094c9f64d6de9a8506b1e49b63a29333b37ed9e821ee04be694d431b3264c3c5"
[[package]]
name = "tempfile"
version = "3.8.0"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"redox_syscall 0.4.1",
"rustix",
"windows-sys",
]
@@ -4783,7 +4798,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -4898,7 +4913,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -4980,9 +4995,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.8"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
dependencies = [
"serde",
"serde_spanned",
@@ -5001,11 +5016,11 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.15"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap 2.0.2",
"indexmap 2.1.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -5060,7 +5075,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -5290,12 +5305,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "waker-fn"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
[[package]]
name = "walkdir"
version = "2.4.0"
@@ -5335,9 +5344,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.87"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -5345,24 +5354,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.87"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.37"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
dependencies = [
"cfg-if",
"js-sys",
@@ -5372,9 +5381,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.87"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -5382,28 +5391,28 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.87"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.87"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
[[package]]
name = "web-sys"
version = "0.3.64"
version = "0.3.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -5582,9 +5591,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.5.17"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b"
dependencies = [
"memchr",
]
@@ -5691,22 +5700,22 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.7.15"
version = "0.7.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f"
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.15"
version = "0.7.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d"
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -5726,5 +5735,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.127.1"
version = "1.129.1"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
@@ -90,7 +90,7 @@ tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.9"
toml = "0.7"
toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }

View File

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

View File

@@ -2941,7 +2941,6 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
* use dc_accounts_remove_account().
*
* @memberof dc_accounts_t
* @param os_name
* @param dir The directory to create the context-databases in.
* If the directory does not exist,
* dc_accounts_new() will try to create it.
@@ -3732,8 +3731,21 @@ int dc_chat_can_send (const dc_chat_t* chat);
/**
* Check if a chat is protected.
* Protected chats contain only verified members and encryption is always enabled.
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
*
* 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 and
* in the chatlist item
* if chat protection is enabled.
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -5033,10 +5045,19 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if a contact was verified. E.g. by a secure-join QR code scan
* and if the key has not changed since this verification.
* Check if the contact
* can be added to verified chats,
* i.e. has a verified key
* and Autocrypt key matches the verified key.
*
* The UI may draw a checkbox or something like that beside verified contacts.
* If contact is verified
* UI should display green checkmark after the contact name
* in contact list items and
* in chat member list items.
*
* Do not use this function when displaying the contact profile view.
* Display green checkmark in the title of the contact profile
* if dc_contact_profile_is_verified() returns true.
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5046,32 +5067,37 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
int dc_contact_is_verified (dc_contact_t* contact);
/**
* Return the address that verified a contact
* Check if the contact profile title
* should contain a green checkmark.
* This indicates whether 1:1 chat with
* this contact has a green checkmark
* or will have if such chat is created.
*
* The UI may use this in addition to a checkmark showing the verification status.
* In case of verification chains,
* the last contact in the chain is shown.
* This is because of privacy reasons, but also as it would not help the user
* to see a unknown name here - where one can mostly always ask the shown name
* as it is directly known.
* Use dc_contact_get_verified_id to display the verifier contact
* in the info section of the contact profile.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return
* A string containing the verifiers address. If it is the same address as the contact itself,
* we verified the contact ourself. If it is an empty string, we don't have verifier
* information or the contact is not verified.
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
* @return 1=contact profile has a green checkmark in the title,
* 0=contact profile has no green checkmark in the title.
*/
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
int dc_contact_profile_is_verified (dc_contact_t* contact);
/**
* Return the `ContactId` that verified a contact
* Return the contact ID that verified a contact.
*
* The UI may use this in addition to a checkmark showing the verification status
* 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.
*
* 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.
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -6234,6 +6260,7 @@ void dc_event_unref(dc_event_t* event);
* @param data2 (int) The progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
* (Bob has verified alice and waits until Alice does the same for him)
* 1000=vg-member-added/vc-contact-confirm received
*/
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
@@ -7260,6 +7287,11 @@ void dc_event_unref(dc_event_t* event);
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/// "Others will only see this group after you sent a first message."
///
/// Used as the first info messages in newly created groups.
#define DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE 172
/**
* @}
*/

View File

@@ -489,7 +489,7 @@ pub unsafe extern "C" fn dc_start_io(context: *mut dc_context_t) {
if context.is_null() {
return;
}
let ctx = &*context;
let ctx = &mut *context;
block_on(ctx.start_io())
}
@@ -4120,20 +4120,18 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_get_verifier_addr(
contact: *mut dc_contact_t,
) -> *mut libc::c_char {
pub unsafe extern "C" fn dc_contact_profile_is_verified(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_verifier_addr()");
return "".strdup();
eprintln!("ignoring careless call to dc_contact_profile_is_verified()");
return 0;
}
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
block_on(ffi_contact.contact.get_verifier_addr(ctx))
.context("failed to get verifier for contact")
block_on(ffi_contact.contact.is_profile_verified(ctx))
.context("is_profile_verified failed")
.log_err(ctx)
.unwrap_or_default()
.strdup()
.unwrap_or_default() as libc::c_int
}
#[no_mangle]
@@ -4946,8 +4944,8 @@ pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
return;
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.start_io().await });
let accounts = &mut *accounts;
block_on(async move { accounts.write().await.start_io().await });
}
#[no_mangle]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.127.1"
version = "1.129.1"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -108,10 +108,10 @@ This will build the `deltachat-jsonrpc-server` binary and then run a test suite
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
Then, set the `DCC_NEW_TMP_EMAIL` environment variable to your mailadm token before running the tests.
Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
```
DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=yourtoken npm run test
CHATMAIL_DOMAIN=chat.example.org npm run test
```
#### Test Coverage

View File

@@ -47,7 +47,7 @@ use types::provider_info::ProviderInfo;
use types::reactions::JSONRPCReactions;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageLoadResult;
use self::types::message::{MessageInfo, MessageLoadResult};
use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
location::JsonrpcLocation,
@@ -221,13 +221,13 @@ impl CommandApi {
/// Starts background tasks for all accounts.
async fn start_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.start_io().await;
self.accounts.write().await.start_io().await;
Ok(())
}
/// Stops background tasks for all accounts.
async fn stop_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.stop_io().await;
self.accounts.write().await.stop_io().await;
Ok(())
}
@@ -237,7 +237,7 @@ impl CommandApi {
/// Starts background tasks for a single account.
async fn start_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let mut ctx = self.get_context(account_id).await?;
ctx.start_io().await;
Ok(())
}
@@ -383,7 +383,7 @@ impl CommandApi {
/// Configures this account with the currently set parameters.
/// Setup the credential config before calling this.
async fn configure(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let mut ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
let result = ctx.configure().await;
if result.is_err() {
@@ -1126,6 +1126,16 @@ impl CommandApi {
MsgId::new(message_id).get_info(&ctx).await
}
/// Returns additional information for single message.
async fn get_message_info_object(
&self,
account_id: u32,
message_id: u32,
) -> Result<MessageInfo> {
let ctx = self.get_context(account_id).await?;
MessageInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
}
/// Returns contacts that sent read receipts and the time of reading.
async fn get_message_read_receipts(
&self,
@@ -1381,6 +1391,19 @@ impl CommandApi {
// chat
// ---------------------------------------------
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
///
/// If it does not exist, `None` is returned.
async fn get_chat_id_by_contact_id(
&self,
account_id: u32,
contact_id: u32,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::lookup_by_contact(&ctx, ContactId::new(contact_id)).await?;
Ok(chat_id.map(|id| id.to_u32()))
}
/// Returns all message IDs of the given types in a chat.
/// Typically used to show a gallery.
///
@@ -2033,13 +2056,19 @@ impl CommandApi {
text: Option<String>,
file: Option<String>,
quoted_message_id: Option<u32>,
view_type: Option<MessageViewtype>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let mut draft = Message::new(if file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
let mut draft = Message::new(view_type.map_or_else(
|| {
if file.is_some() {
Viewtype::File
} else {
Viewtype::Text
}
},
|v| v.into(),
));
draft.set_text(text.unwrap_or_default());
if let Some(file) = file {
draft.set_file(file, None);

View File

@@ -18,6 +18,17 @@ use super::contact::ContactObject;
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,
profile_image: Option<String>, //BLOBS ?
archived: bool,
@@ -128,6 +139,17 @@ impl FullChat {
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,
profile_image: Option<String>, //BLOBS ?
archived: bool,

View File

@@ -19,11 +19,29 @@ pub struct ContactObject {
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
/// True if the contact can be added to verified groups.
///
/// If this is true
/// UI should display green checkmark after the contact name
/// in contact list items and
/// in chat member list items.
is_verified: bool,
/// the address that verified this contact
verifier_addr: Option<String>,
/// the id of the contact that verified this contact
/// True if the contact profile title should have a green checkmark.
///
/// 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.
///
/// If this is present,
/// display a green checkmark and "Introduced by ..."
/// string followed by the verifier contact name and address
/// in the contact profile.
verifier_id: Option<u32>,
/// the contact's last seen timestamp
last_seen: i64,
was_seen_recently: bool,
@@ -39,18 +57,12 @@ impl ContactObject {
None => None,
};
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
let is_profile_verified = contact.is_profile_verified(context).await?;
let (verifier_addr, verifier_id) = if is_verified {
(
contact.get_verifier_addr(context).await?,
contact
.get_verifier_id(context)
.await?
.map(|contact_id| contact_id.to_u32()),
)
} else {
(None, None)
};
let verifier_id = contact
.get_verifier_id(context)
.await?
.map(|contact_id| contact_id.to_u32());
Ok(ContactObject {
address: contact.get_addr().to_owned(),
@@ -64,7 +76,7 @@ impl ContactObject {
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_verified,
verifier_addr,
is_profile_verified,
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),

View File

@@ -552,3 +552,71 @@ pub struct MessageReadReceipt {
pub contact_id: u32,
pub timestamp: i64,
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageInfo {
rawtext: String,
ephemeral_timer: EphemeralTimer,
/// When message is ephemeral this contains the timestamp of the message expiry
ephemeral_timestamp: Option<i64>,
error: Option<String>,
rfc724_mid: String,
server_urls: Vec<String>,
hop_info: Option<String>,
}
impl MessageInfo {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let rawtext = msg_id.rawtext(context).await?;
let ephemeral_timer = message.get_ephemeral_timer().into();
let ephemeral_timestamp = match message.get_ephemeral_timer() {
deltachat::ephemeral::Timer::Disabled => None,
deltachat::ephemeral::Timer::Enabled { .. } => Some(message.get_ephemeral_timestamp()),
};
let server_urls =
MsgId::get_info_server_urls(context, message.rfc724_mid().to_owned()).await?;
let hop_info = msg_id.hop_info(context).await?;
Ok(Self {
rawtext,
ephemeral_timer,
ephemeral_timestamp,
error: message.error(),
rfc724_mid: message.rfc724_mid().to_owned(),
server_urls,
hop_info,
})
}
}
#[derive(
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
)]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum EphemeralTimer {
/// Timer is disabled.
Disabled,
/// Timer is enabled.
Enabled {
/// Timer duration in seconds.
///
/// The value cannot be 0.
duration: u32,
},
}
impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
fn from(value: deltachat::ephemeral::Timer) -> Self {
match value {
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
deltachat::ephemeral::Timer::Enabled { duration } => {
EphemeralTimer::Enabled { duration }
}
}
}
}

View File

@@ -28,7 +28,7 @@ async fn main() -> Result<(), std::io::Error> {
.layer(Extension(state.clone()));
tokio::spawn(async move {
state.accounts.read().await.start_io().await;
state.accounts.write().await.start_io().await;
});
let addr = SocketAddr::from(([127, 0, 0, 1], port));

View File

@@ -9,7 +9,6 @@
"@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.0.0",
"@types/node-fetch": "^2.5.7",
"@types/ws": "^7.2.4",
"c8": "^7.10.0",
"chai": "^4.3.4",
@@ -17,7 +16,6 @@
"esbuild": "^0.17.9",
"http-server": "^14.1.1",
"mocha": "^9.1.1",
"node-fetch": "^2.6.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"typedoc": "^0.23.2",
@@ -55,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.127.1"
"version": "1.129.1"
}

View File

@@ -79,6 +79,9 @@ describe("basic tests", () => {
accountId = await dc.rpc.addAccount();
});
it("should block and unblock contact", async function () {
// Cannot send sync messages to self as we do not have a self address.
await dc.rpc.setConfig(accountId, "sync_msgs", "0");
const contactId = await dc.rpc.createContact(
accountId,
"example@delta.chat",

View File

@@ -13,16 +13,16 @@ describe("online tests", function () {
before(async function () {
this.timeout(60000);
if (!process.env.DCC_NEW_TMP_EMAIL) {
if (!process.env.CHATMAIL_DOMAIN) {
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
console.error(
"CAN NOT RUN COVERAGE correctly: Missing DCC_NEW_TMP_EMAIL environment variable!\n\n",
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
);
process.exit(1);
}
console.log(
"Missing DCC_NEW_TMP_EMAIL environment variable!, skip integration tests"
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
);
this.skip();
}
@@ -33,7 +33,7 @@ describe("online tests", function () {
if (kind !== "Info") console.log(contextId, kind);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
if (!account1 || !account1.email || !account1.password) {
console.log(
"We didn't got back an account from the api, skip integration tests"
@@ -41,7 +41,7 @@ describe("online tests", function () {
this.skip();
}
account2 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
if (!account2 || !account2.email || !account2.password) {
console.log(
"We didn't got back an account2 from the api, skip integration tests"

View File

@@ -2,7 +2,6 @@ import { tmpdir } from "os";
import { join, resolve } from "path";
import { mkdtemp, rm } from "fs/promises";
import { spawn, exec } from "child_process";
import fetch from "node-fetch";
import { Readable, Writable } from "node:stream";
export type RpcServerHandle = {
@@ -57,15 +56,14 @@ export async function startServer(): Promise<RpcServerHandle> {
};
}
export async function createTempUser(url: string) {
const response = await fetch(url, {
method: "POST",
headers: {
"cache-control": "no-cache",
},
});
if (!response.ok) throw new Error("Received invalid response");
return response.json();
export function createTempUser(chatmailDomain: String) {
const charset = "2345789acdefghjkmnpqrstuvwxyz";
let user = "ci-";
for (let i = 0; i < 6; i++) {
user += charset[Math.floor(Math.random() * charset.length)];
}
const email = user + "@" + chatmailDomain;
return { email: email, password: user + "$" + user };
}
function getTargetDir(): Promise<string> {

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.127.1"
version = "1.129.1"
license = "MPL-2.0"
edition = "2021"

View File

@@ -401,7 +401,7 @@ enum ExitResult {
async fn handle_cmd(
line: &str,
ctx: Context,
mut ctx: Context,
selected_chat: &mut ChatId,
) -> Result<ExitResult, Error> {
let mut args = line.splitn(2, ' ');

View File

@@ -4,7 +4,7 @@ from warnings import warn
from ._utils import AttrDict
from .chat import Chat
from .const import ChatlistFlag, ContactFlag, SpecialContactId
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
from .contact import Contact
from .message import Message
@@ -250,6 +250,13 @@ class Account:
next_msg_ids = self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def wait_for_incoming_msg_event(self):
"""Wait for incoming message event and return it."""
while True:
event = self.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
return event
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(

View File

@@ -160,11 +160,12 @@ class Chat:
text: Optional[str] = None,
file: Optional[str] = None,
quoted_msg: Optional[int] = None,
viewtype: Optional[str] = None,
) -> None:
"""Set draft message."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype)
def remove_draft(self) -> None:
"""Remove draft message."""

View File

@@ -1,6 +1,5 @@
import json
import os
import urllib.request
import random
from typing import AsyncGenerator, List, Optional
import pytest
@@ -10,12 +9,11 @@ from .rpc import Rpc
def get_temp_credentials() -> dict:
url = os.getenv("DCC_NEW_TMP_EMAIL")
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
request = urllib.request.Request(url, method="POST")
with urllib.request.urlopen(request, timeout=60) as f:
return json.load(f)
domain = os.getenv("CHATMAIL_DOMAIN")
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
password = f"{username}${username}"
addr = f"{username}@{domain}"
return {"email": addr, "password": password}
class ACFactory:
@@ -23,7 +21,9 @@ class ACFactory:
self.deltachat = deltachat
def get_unconfigured_account(self) -> Account:
return self.deltachat.add_account()
account = self.deltachat.add_account()
account.set_config("verified_one_on_one_chats", "1")
return account
def get_unconfigured_bot(self) -> Bot:
return Bot(self.get_unconfigured_account())
@@ -61,6 +61,16 @@ class ACFactory:
def get_online_accounts(self, num: int) -> List[Account]:
return [self.get_online_account() for _ in range(num)]
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))
ac.remove()
ac_clone.configure()
return ac_clone
def send_message(
self,
to_account: Account,

View File

@@ -1,3 +1,4 @@
import itertools
import json
import logging
import os
@@ -5,7 +6,7 @@ import subprocess
import sys
from queue import Queue
from threading import Event, Thread
from typing import Any, Dict, Optional
from typing import Any, Dict, Iterator, Optional
class JsonRpcError(Exception):
@@ -23,7 +24,7 @@ class Rpc:
self._kwargs = kwargs
self.process: subprocess.Popen
self.id: int
self.id_iterator: Iterator[int]
self.event_queues: Dict[int, Queue]
# Map from request ID to `threading.Event`.
self.request_events: Dict[int, Event]
@@ -54,7 +55,7 @@ class Rpc:
preexec_fn=os.setpgrp, # noqa: PLW1509
**self._kwargs,
)
self.id = 0
self.id_iterator = itertools.count(start=1)
self.event_queues = {}
self.request_events = {}
self.request_results = {}
@@ -131,7 +132,9 @@ class Rpc:
event = self.get_next_event()
account_id = event["contextId"]
queue = self.get_queue(account_id)
queue.put(event["event"])
event = event["event"]
logging.debug("account_id=%d got an event %s", account_id, event)
queue.put(event)
except Exception:
# Log an exception if the event loop dies.
logging.exception("Exception in the event loop")
@@ -143,14 +146,12 @@ class Rpc:
def __getattr__(self, attr: str):
def method(*args) -> Any:
self.id += 1
request_id = self.id
request_id = next(self.id_iterator)
request = {
"jsonrpc": "2.0",
"method": attr,
"params": args,
"id": self.id,
"id": request_id,
}
event = Event()
self.request_events[request_id] = event

View File

@@ -0,0 +1,252 @@
import logging
import pytest
from deltachat_rpc_client import Chat, SpecialContactId
def test_qr_setup_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
assert alice_contact_bob_snapshot.is_profile_verified
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
assert bob_contact_alice_snapshot.is_profile_verified
def test_qr_securejoin(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
assert alice_contact_bob_snapshot.is_profile_verified
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
assert bob_contact_alice_snapshot.is_profile_verified
@pytest.mark.xfail()
def test_verified_group_recovery(acfactory, rpc) -> None:
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("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac3.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
# ac1 contact cannot be verified by ac2 because ac3 did not gossip ac1 key in the "Hi!" message.
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
assert not ac1_contact.get_snapshot().is_verified
ac3_contact_id_ac1 = rpc.lookup_contact_id_by_addr(ac3.id, ac1.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_id_ac1)
ac3_chat.add_contact(ac3_contact_id_ac1)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "removed" in snapshot.text
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "added" in snapshot.text
assert ac1_contact.get_snapshot().is_verified
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)
snapshot = message.get_snapshot()
assert snapshot.text == "Works again!"
ac1.wait_for_incoming_msg_event() # Hi!
ac1.wait_for_incoming_msg_event() # Member removed
ac1.wait_for_incoming_msg_event() # Member added
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
def test_verified_group_member_added_recovery(acfactory) -> None:
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("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac3.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
ac1.wait_for_incoming_msg_event() # Hi!
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_ac2)
ac3_chat.add_contact(ac3_contact_ac2)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert "removed" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
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()
assert "added" in snapshot.text
logging.info("ac2 address is %s", ac2.get_config("addr"))
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
# assert ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
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)
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()
assert snapshot.text == "Works again!"
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id

View File

@@ -377,15 +377,3 @@ def test_provider_info(rpc) -> None:
rpc.set_config(account_id, "socks5_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
def test_qr_setup_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
return

View File

@@ -11,7 +11,7 @@ setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
passenv =
DCC_NEW_TMP_EMAIL
CHATMAIL_DOMAIN
deps =
pytest
pytest-timeout
@@ -28,6 +28,6 @@ commands =
ruff src/ examples/ tests/
[pytest]
timeout = 60
timeout = 300
log_cli = true
log_level = info
log_level = debug

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.127.1"
version = "1.129.1"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -17,7 +17,6 @@ anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "2.0.0"
log = "0.4"
num_cpus = "1"
serde_json = "1.0.105"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.33.0", features = ["io-std"] }

View File

@@ -20,18 +20,9 @@ use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use yerpc::{RpcClient, RpcSession};
fn main() {
// Build multithreaded runtime with at least two threads.
// This ensures that on systems with one CPU
// such as CI runners there are at least two threads
// and it is more difficult to deadlock.
let r = tokio::runtime::Builder::new_multi_thread()
.worker_threads(std::cmp::max(2, num_cpus::get()))
.enable_all()
.build()
.unwrap()
.block_on(main_impl());
#[tokio::main(flavor = "multi_thread")]
async fn main() {
let r = main_impl().await;
// From tokio documentation:
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang

View File

@@ -35,10 +35,10 @@ skip = [
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "ring", version = "0.16.20" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "ring", version = "0.16.20" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },

View File

@@ -57,7 +57,7 @@ Note that usually a mail is signed by a key that has a UID matching the from add
### Notes:
- We treat protected and non-protected chats the same
- We leave the aeap transition statement away since it seems not to be needed, makes things harder on the sending side, wastes some network traffic, and is worse for privacy (since more pepole know what old addresses you had).
- We leave the aeap transition statement away since it seems not to be needed, makes things harder on the sending side, wastes some network traffic, and is worse for privacy (since more people know what old addresses you had).
- As soon as we encrypt read receipts, sending a read receipt will be enough to tell a lot of people that you transitioned
- AEAP will make the problem of inconsistent group state worse, both because it doesn't work if the message is unencrypted (even if the design allowed it, it would be problematic security-wise) and because some chat partners may have gotten the transition and some not. We should do something against this at some point in the future, like asking the user whether they want to add/remove the members to restore consistent group state.

129
draft/keymanagement.md Normal file
View File

@@ -0,0 +1,129 @@
# New acpeerstates table
A proposal to replace `acpeerstates` table,
which has columns
- `id`
- `addr`
- `prefer_encrypted`
- `last_seen`
- `last_seen_autocrypt`
- `public_key`
- `public_key_fingrprint`
- `gossip_timestamp`
- `gossip_key`
- `gossip_key_fingerprint`
- `verified_key`
- `verified_key_fingerprint`
- `verifier`
- `secondary_verified_key`
- `secondary_verified_key_fingerprint`
- `secondary_verifier`
with a new `public_keys` table with columns
- `id`
- `addr` - address of the contact which is supposed to have the private key
- `public_key` - a blob with the OpenPGP public key
- `public_key_fingerprint` - public key fingerprint
- `introduced_by` - address of the contact if received directly in `Autocrypt` header, address of `Autocrypt-Gossip` sender otherwise
- `is_verified` - boolean flag indicating if the key was received in a verified group or not
- `timestamp` - timestamp of the most recent row update
The table has consraints `UNIQUE(addr, introduced_by)`.
Note that there may be multiple rows with the same fingerprint,
e.g. if multiple peers gossiped the same key for a contact they all are introducers of the key.
`prefer_encrypted` is moved to the `contacts` table.
## Using the table to select the keys
### Sending a message to 1:1 chat
When sending a message to the contact in a 1:1 chat,
encrypt to the key where `introduced_by` equals `addr`:
```
SELECT *
FROM public_keys
WHERE addr=? AND introduced_by=addr
```
This row is guaranteed to be unique if it exists.
If direct Autocrypt key does not exist,
use the most recent one according to the timestamp key gossiped for this contact:
```
SELECT *
FROM public_keys
WHERE addr=?
ORDER BY timestamp DESC LIMIT 1
```
If the table contains a row with selected key and `is_verified` flag set,
display a green checkmark and "Introduced by <introduced_by>" in the 1:1 chat/contact profile.
Note that row with verification may be older than the most recent gossip.
### Sending a message to unprotected group chat
When sending a message to the contact in a non-verified group chat,
encrypt to the same key as used in 1:1 chat
and (NEW!) one or more most recent gossiped keys introduced by chat members:
```
SELECT public_key
FROM public_keys
WHERE addr=? AND introduced_by IN (<chat-member-list>)
ORDER BY timestamp DESC
```
It does not matter if the gossiped key has `is_verified` flag.
### Sending a message to protected group chat
When sending a message in a protected group chat,
construct a list of candidate keys for each contact
the same way as in an unprotected chat,
but then filter out the keys which do not have `is_verified` flag.
## Updating the table
When executing "setup contact" protocol
with the contact,
set the row with `addr` and `introduced_by` being equal
to the contact address and `is_verified` is true.
In this case the contact becomes directly verified.
When receiving a message,
first process the `Autocrypt` key,
then check verification properties to detect if gossip is signed with a verified key,
then process `Autocrypt-Gossip` key.
### 1. Processing the Autocrypt header
Take the key from the Autocrypt header
and insert or update the row
identified by `addr` and `introduced_by` being equal to the `From:` address.
Notify about setup change and reset `is_verified` flag if the key fingerprint has changed.
Always update the timestamp.
Update `prefer_encrypted` in the contacts table as needed
based on the `Autocrypt` header parameters,
whether the message is signed etc.
### 2. Check verification
Determine whether the green checkmark should now be displayed
on the 1:1 chat and update it accordingly,
insert system messages,
get it into broken state or out of it.
Now we know if the message is signed with a verified key.
### 3. Process `Autocrypt-Gossip`
Insert or update gossip keys
```
INSERT INTO public_keys (addr, public_key, public_key_fingerprint, introduced_by, is_verified, timestamp)
VALUES (?, ?, ?, <introducer>, ?, <now>)
ON CONFLICT
DO UPDATE SET public_key=excluded.public_key, public_key_fingerprint=excluded.public_key_fingerprint
```
`is_verified` should be set if the message is signed with a verified key,
independently of whether the key was gossiped in a protected chat or not.

View File

@@ -40,7 +40,7 @@ npm install deltachat-node
## Dependencies
- Nodejs >= `v16.0.0`
- Nodejs >= `v18.0.0`
- rustup (optional if you can't use the prebuilds)
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
@@ -113,8 +113,8 @@ Then, in the `deltachat-desktop` repository, run:
deltachat doesn't support universal (fat) binaries (that contain builds for both cpu architectures) yet, until it does you can use the following workaround to get x86_64 builds:
```
$ fnm install 17 --arch x64
$ fnm use 17
$ fnm install 19 --arch x64
$ fnm use 19
$ node -p process.arch
# result should be x64
$ rustup target add x86_64-apple-darwin
@@ -127,8 +127,8 @@ $ npm run test
If your node and electron are already build for arm64 you can also try building for arm:
```
$ fnm install 16 --arch arm64
$ fnm use 16
$ fnm install 18 --arch arm64
$ fnm use 18
$ node -p process.arch
# result should be arm64
$ npm_config_arch=arm64 npm run build
@@ -204,10 +204,10 @@ Running `npm test` ends with showing a code coverage report, which is produced b
The coverage report from `nyc` in the console is rather limited. To get a more detailed coverage report you can run `npm run coverage-html-report`. This will produce a html report from the `nyc` data and display it in a browser on your local machine.
To run the integration tests you need to set the `DCC_NEW_TMP_EMAIL` environment variables. E.g.:
To run the integration tests you need to set the `CHATMAIL_DOMAIN` environment variables. E.g.:
```
$ export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=[token]
$ export CHATMAIL_DOMAIN=chat.example.org
$ npm run test
```

View File

@@ -239,6 +239,7 @@ module.exports = {
DC_STR_MSGGRPNAME: 15,
DC_STR_MSGLOCATIONDISABLED: 65,
DC_STR_MSGLOCATIONENABLED: 64,
DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE: 172,
DC_STR_NOMESSAGES: 1,
DC_STR_NOT_CONNECTED: 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER: 113,

View File

@@ -239,6 +239,7 @@ export enum C {
DC_STR_MSGGRPNAME = 15,
DC_STR_MSGLOCATIONDISABLED = 65,
DC_STR_MSGLOCATIONENABLED = 64,
DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE = 172,
DC_STR_NOMESSAGES = 1,
DC_STR_NOT_CONNECTED = 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,

View File

@@ -8,26 +8,17 @@ import { EventId2EventName, C } from '../dist/constants'
import { join } from 'path'
import { statSync } from 'fs'
import { Context } from '../dist/context'
import fetch from 'node-fetch'
chai.use(chaiAsPromised)
chai.config.truncateThreshold = 0 // Do not truncate assertion errors.
async function createTempUser(url) {
async function postData(url = '') {
// Default options are marked with *
const response = await fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
headers: {
'cache-control': 'no-cache',
},
})
if (!response.ok) {
throw new Error('request failed: ' + response.body.read())
}
return response.json() // parses JSON response into native JavaScript objects
function createTempUser(chatmailDomain) {
const charset = "2345789acdefghjkmnpqrstuvwxyz";
let user = "ci-";
for (let i = 0; i < 6; i++) {
user += charset[Math.floor(Math.random() * charset.length)];
}
return await postData(url)
const email = user + "@" + chatmailDomain;
return { email: email, password: user + "$" + user };
}
describe('static tests', function () {
@@ -676,9 +667,9 @@ describe('Offline Tests with unconfigured account', function () {
const lot = chatList.getSummary(0)
strictEqual(lot.getId(), 0, 'lot has no id')
strictEqual(lot.getState(), C.DC_STATE_UNDEFINED, 'correct state')
strictEqual(lot.getState(), C.DC_STATE_IN_NOTICED, 'correct state')
const text = 'No messages.'
const text = 'Others will only see this group after you sent a first message.'
context.createGroupChat('groupchat1111')
chatList = context.getChatList(0, 'groupchat1111', null)
strictEqual(
@@ -768,14 +759,7 @@ describe('Integration tests', function () {
})
this.beforeAll(async function () {
if (!process.env.DCC_NEW_TMP_EMAIL) {
console.log(
'Missing DCC_NEW_TMP_EMAIL environment variable!, skip integration tests'
)
this.skip()
}
account = await createTempUser(process.env.DCC_NEW_TMP_EMAIL)
account = createTempUser(process.env.CHATMAIL_DOMAIN)
if (!account || !account.email || !account.password) {
console.log(
"We didn't got back an account from the api, skip integration tests"

View File

@@ -13,10 +13,10 @@
},
"exclude": ["node_modules", "deltachat-core-rust", "dist", "scripts"],
"typedocOptions": {
"mode": "file",
"out": "docs",
"excludeNotExported": true,
"excludePrivate": true,
"defaultCategory": "index",
"includeVersion": true
"includeVersion": true,
"entryPoints": ["lib/index.ts"]
}
}

View File

@@ -6,7 +6,7 @@ E.g via <https://git-scm.com/download/win>
## install node
Download and install `v16` from <https://nodejs.org/en/>
Download and install `v18` from <https://nodejs.org/en/>
## install rust

View File

@@ -2,28 +2,25 @@
"dependencies": {
"debug": "^4.1.1",
"napi-macros": "^2.0.0",
"node-gyp-build": "^4.1.0"
"node-gyp-build": "^4.6.1"
},
"description": "node.js bindings for deltachat-core",
"devDependencies": {
"@types/debug": "^4.1.7",
"@types/node": "^16.11.26",
"@types/node": "^20.8.10",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"esm": "^3.2.25",
"hallmark": "^2.0.0",
"mocha": "^8.2.1",
"node-fetch": "^2.6.7",
"node-gyp": "^9.0.0",
"opn-cli": "^5.0.0",
"prebuildify": "^3.0.0",
"prebuildify-ci": "^1.0.4",
"prettier": "^2.0.5",
"typedoc": "^0.17.0",
"typescript": "^3.9.10"
"node-gyp": "^10.0.0",
"prebuildify": "^5.0.1",
"prebuildify-ci": "^1.0.5",
"prettier": "^3.0.3",
"typedoc": "^0.25.3",
"typescript": "^5.2.2"
},
"engines": {
"node": ">=16.0.0"
"node": ">=18.0.0"
},
"files": [
"node/scripts/*",
@@ -49,16 +46,15 @@
"build:core:rust": "node node/scripts/rebuild-core.js",
"clean": "rm -rf node/dist node/build node/prebuilds node/node_modules ./target",
"download-prebuilds": "prebuildify-ci download",
"hallmark": "hallmark --fix",
"install": "node node/scripts/install.js",
"install:prebuilds": "cd node && node-gyp-build \"npm run build:core\" \"npm run build:bindings:c:postinstall\"",
"lint": "prettier --check \"node/lib/**/*.{ts,tsx}\"",
"lint-fix": "prettier --write \"node/lib/**/*.{ts,tsx}\" \"node/test/**/*.js\"",
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
"prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
"test": "npm run test:lint && npm run test:mocha",
"test:lint": "npm run lint",
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.127.1"
"version": "1.129.1"
}

View File

@@ -53,14 +53,14 @@ end-to-end tests that require accounts on real e-mail servers.
Running "live" tests with temporary accounts
--------------------------------------------
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLs created and managed by `mailadm <https://mailadm.readthedocs.io/>`_.
If you want to run live functional tests
you can set ``CHATMAIL_DOMAIN`` to a domain of the email server
that creates e-mail accounts like this::
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this::
export CHATMAIL_DOMAIN=nine.testrun.org
export DCC_NEW_TMP_EMAIL=<URL you got from us>
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
These accounts are removed automatically as they expire.
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the server.
These accounts have the pattern `ci-{6 characters}@{CHATMAIL_DOMAIN}`.
After setting the variable, either rerun `scripts/run-python-test.sh`
or run offline and online tests with `tox` directly::
@@ -159,6 +159,6 @@ find it with::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_NEW_TMP_EMAIL \
$ docker run -e CHATMAIL_DOMAIN \
--rm -it -v $(pwd):/mnt -w /mnt \
deltachat/coredeps scripts/run_all.sh

View File

@@ -571,11 +571,11 @@ class Account:
return ScannedQRCode(lot)
def qr_setup_contact(self, qr):
"""setup contact and return a Chat after contact is established.
"""setup contact and return a `Chat` instance after contact is established.
This function triggers a network protocol in the background between
the emitter of the QR code and this account.
Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
is returned.
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
"""
assert self.check_qr(qr).is_ask_verifycontact()
@@ -585,11 +585,11 @@ class Account:
return Chat(self, chat_id)
def qr_join_chat(self, qr):
"""join a chat group through a QR code.
"""return a `Chat` instance for which the securejoin network
protocol has been started.
Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
is returned which is the chat that we just joined.
This function triggers a network protocol in the background between
the emitter of the QR code and this account.
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
"""

View File

@@ -8,11 +8,11 @@ import sys
import threading
import time
import weakref
import random
from queue import Queue
from typing import Callable, Dict, List, Optional, Set
import pytest
import requests
from _pytest._code import Source
import deltachat
@@ -29,6 +29,12 @@ def pytest_addoption(parser):
default=None,
help="a file with >=2 lines where each line contains NAME=VALUE config settings for one account",
)
group.addoption(
"--chatmail",
action="store",
default=None,
help="chatmail server domain name",
)
group.addoption(
"--ignored",
action="store_true",
@@ -53,10 +59,12 @@ def pytest_addoption(parser):
def pytest_configure(config):
cfg = config.getoption("--liveconfig")
cfg = config.getoption("--chatmail")
if not cfg:
cfg = os.getenv("DCC_NEW_TMP_EMAIL")
cfg = os.getenv("CHATMAIL_DOMAIN")
if cfg:
config.option.liveconfig = cfg
config.option.chatmail = cfg
# Make sure we don't get garbled output because threads keep running
# collect all ever created accounts in a weakref-set (so we don't
@@ -157,13 +165,29 @@ class TestProcess:
"""provide live account configs, cached on a per-test-process scope
so that test functions can re-use already known live configs.
Depending on the --liveconfig option this comes from
a HTTP provider or a file with a line specifying each accounts config.
a file with a line specifying each accounts config
or a --chatmail domain.
"""
liveconfig_opt = self.pytestconfig.getoption("--liveconfig")
if not liveconfig_opt:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts")
if not liveconfig_opt.startswith("http"):
chatmail_opt = self.pytestconfig.getoption("--chatmail")
if chatmail_opt:
# Use a chatmail instance.
domain = chatmail_opt
MAX_LIVE_CREATED_ACCOUNTS = 10
for index in range(MAX_LIVE_CREATED_ACCOUNTS):
try:
yield self._configlist[index]
except IndexError:
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
password = f"{username}${username}"
addr = f"{username}@{domain}"
config = {"addr": addr, "mail_pw": password}
print("newtmpuser {}: addr={}".format(index, config["addr"]))
self._configlist.append(config)
yield config
pytest.fail(f"more than {MAX_LIVE_CREATED_ACCOUNTS} live accounts requested.")
elif liveconfig_opt:
# Read a list of accounts from file.
for line in open(liveconfig_opt):
if line.strip() and not line.strip().startswith("#"):
d = {}
@@ -174,20 +198,9 @@ class TestProcess:
yield from iter(self._configlist)
else:
MAX_LIVE_CREATED_ACCOUNTS = 10
for index in range(MAX_LIVE_CREATED_ACCOUNTS):
try:
yield self._configlist[index]
except IndexError:
res = requests.post(liveconfig_opt, timeout=60)
if res.status_code != 200:
pytest.fail(f"newtmpuser count={index} code={res.status_code}: '{res.text}'")
d = res.json()
config = {"addr": d["email"], "mail_pw": d["password"]}
print("newtmpuser {}: addr={}".format(index, config["addr"]))
self._configlist.append(config)
yield config
pytest.fail(f"more than {MAX_LIVE_CREATED_ACCOUNTS} live accounts requested.")
pytest.skip(
"specify --liveconfig, CHATMAIL_DOMAIN or --chatmail to provide live accounts",
)
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
db_target_path = pathlib.Path(db_target_path)
@@ -533,6 +546,7 @@ class ACFactory:
configdict.setdefault("bcc_self", False)
configdict.setdefault("mvbox_move", False)
configdict.setdefault("sentbox_watch", False)
configdict.setdefault("sync_msgs", False)
ac.update_config(configdict)
self._preconfigure_key(ac, configdict["addr"])
return ac

View File

@@ -367,7 +367,7 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
lp.sec("ac2 sets download limit")
ac2.set_config("download_limit", "100")
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(300000))}, "some test data")
ac2_update = ac2._evtracker.wait_next_incoming_message()
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert not msg2.get_status_updates()
@@ -489,7 +489,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) == 1
assert len(messages) == 2
msg = messages[-1]
assert msg.is_forwarded()
ac2.delete_messages(messages)
@@ -1445,7 +1445,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 32768
download_limit = 300000
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
@@ -1699,6 +1699,59 @@ def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
assert not ch.is_protected()
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory, lp):
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
lp.sec("ac3: verify with ac2")
ac3.qr_setup_contact(ac2.get_setup_contact_qr())
ac2._evtracker.wait_securejoin_inviter_progress(1000)
# in order for ac2 to have pending bobstate with a verified group
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
lp.sec("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group_chat("ac1-shutoff group", verified=True)
ac2.qr_join_chat(ch1.get_join_qr())
ac1._evtracker.wait_securejoin_inviter_progress(1000)
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
msg = ac2.wait_next_incoming_message()
if msg.text == "ac1 says hello":
assert msg.chat.is_protected()
break
lp.sec("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
ac2.qr_join_chat(ch1.get_join_qr())
ac1.shutdown()
lp.sec("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.get_contact(ac2).is_verified()
assert ac2.get_contact(ac3).is_verified()
lp.sec("ac3: create a verified group VG with ac2")
vg = ac3.create_group_chat("ac3-created", [ac2], verified=True)
# ensure ac2 receives message in VG
vg.send_text("hello")
while 1:
msg = ac2.wait_next_incoming_message()
if msg.text == "hello":
assert msg.chat.is_protected()
break
lp.sec("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
ac4.qr_join_chat(vg.get_join_qr())
ac3._evtracker.wait_securejoin_inviter_progress(1000)
while 1:
ev = ac2._evtracker.get()
if "added by unrelated SecureJoin" in str(ev):
return
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
@@ -1904,7 +1957,7 @@ def test_system_group_msg_from_blocked_user(acfactory, lp):
chat_on_ac2.send_text("This will arrive")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "This will arrive"
message_texts = [m.text for m in chat_on_ac1.get_messages()]
message_texts = [m.text for m in chat_on_ac1.get_messages() if not m.is_system_message()]
assert len(message_texts) == 2
assert "First group message" in message_texts
assert "This will arrive" in message_texts
@@ -2208,7 +2261,17 @@ def test_delete_multiple_messages(acfactory, lp):
def test_trash_multiple_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
# Create the Trash folder on IMAP server
# and recreate the account so Trash folder is configured.
lp.sec("Creating trash folder")
ac2.direct_imap.create_folder("Trash")
lp.sec("Creating new accounts")
ac2 = acfactory.new_online_configuring_account(cloned_from=ac2)
acfactory.bring_accounts_online()
ac2.set_config("delete_to_trash", "1")
assert ac2.get_config("configured_trash_folder")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: sending 3 messages")
@@ -2239,13 +2302,15 @@ def test_trash_multiple_messages(acfactory, lp):
def test_configure_error_msgs_wrong_pw(acfactory):
configdict = acfactory.get_next_liveconfig()
ac1 = acfactory.get_unconfigured_account()
ac1.update_config(configdict)
ac1.set_config("mail_pw", "abc") # Wrong mail pw
ac1.configure()
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.get_unconfigured_account()
ac2.set_config("addr", ac1.get_config("addr"))
ac2.set_config("mail_pw", "abc") # Wrong mail pw
ac2.configure()
while True:
ev = ac1._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
ev = ac2._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
print(f"Configuration progress: {ev.data1}")
if ev.data1 == 0:
break
# Password is wrong so it definitely has to say something about "password"
@@ -2548,7 +2613,7 @@ class TestOnlineConfigureFails:
def test_invalid_user(self, acfactory):
configdict = acfactory.get_next_liveconfig()
ac1 = acfactory.get_unconfigured_account()
configdict["addr"] = "x" + configdict["addr"]
configdict["addr"] = "$" + configdict["addr"]
ac1.update_config(configdict)
configtracker = ac1.configure()
configtracker.wait_progress(500)
@@ -2557,7 +2622,7 @@ class TestOnlineConfigureFails:
def test_invalid_domain(self, acfactory):
configdict = acfactory.get_next_liveconfig()
ac1 = acfactory.get_unconfigured_account()
configdict["addr"] += "x"
configdict["addr"] += "$"
ac1.update_config(configdict)
configtracker = ac1.configure()
configtracker.wait_progress(500)

View File

@@ -26,7 +26,7 @@ def wait_msgs_changed(account, msgs_list):
break
else:
account.log(f"waiting mismatch data1={data1} data2={data2}")
return ev.data1, ev.data2
return ev.data2
class TestOnlineInCreation:
@@ -68,14 +68,17 @@ class TestOnlineInCreation:
assert prepared_original.is_out_preparing()
wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
lp.sec("forward the message while still in creation")
lp.sec("create a new group")
chat2 = ac1.create_group_chat("newgroup")
wait_msgs_changed(ac1, [(0, 0)])
lp.sec("add a contact to new group")
chat2.add_contact(ac2)
wait_msgs_changed(ac1, [(0, 0)]) # why not chat id?
wait_msgs_changed(ac1, [(chat2.id, None)])
lp.sec("forward the message while still in creation")
ac1.forward_messages([prepared_original], chat2)
# XXX there might be two EVENT_MSGS_CHANGED and only one of them
# is the one caused by forwarding
_, forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
forwarded_msg = ac1.get_message_by_id(forwarded_id)
assert forwarded_msg.is_out_preparing()

View File

@@ -5,8 +5,6 @@ from datetime import datetime, timedelta, timezone
import pytest
import deltachat as dc
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from deltachat.tracker import ImexFailed
from deltachat import Account, account_hookimpl, Message
@@ -802,6 +800,7 @@ class TestOfflineChat:
def test_audit_log_view_without_daymarker(self, ac1, lp):
lp.sec("ac1: test audit log (show only system messages)")
chat = ac1.create_group_chat(name="audit log sample data")
# promote chat
chat.send_text("hello")
assert chat.is_promoted()
@@ -811,12 +810,6 @@ class TestOfflineChat:
chat.set_name("audit log test group")
chat.send_text("a message in between")
lp.sec("check message count of all messages")
assert len(chat.get_messages()) == 4
lp.sec("check message count of only system messages (without daymarkers)")
dc_array = ffi.gc(
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
lib.dc_array_unref,
)
assert len(list(iter_array(dc_array, lambda x: x))) == 2
sysmessages = [x for x in chat.get_messages() if x.is_system_message()]
assert len(sysmessages) == 3

View File

@@ -16,7 +16,7 @@ setenv =
passenv =
DCC_RS_DEV
DCC_RS_TARGET
DCC_NEW_TMP_EMAIL
CHATMAIL_DOMAIN
CARGO_TARGET_DIR
RUSTC_WRAPPER
deps =
@@ -85,7 +85,7 @@ commands =
addopts = -v -ra --strict-markers
norecursedirs = .tox
xfail_strict=true
timeout = 150
timeout = 300
timeout_func_only = True
markers =
ignored: ignore this test in default test runs, use --ignored to run.

View File

@@ -1 +1 @@
2023-10-27
2023-11-06

View File

@@ -3,4 +3,4 @@ set -euo pipefail
tox -c deltachat-rpc-client -e py --devenv venv
venv/bin/pip install --upgrade pip
cargo install --path deltachat-rpc-server/ --root "$PWD/venv"
cargo install --path deltachat-rpc-server/ --root "$PWD/venv" --debug

View File

@@ -28,7 +28,7 @@ ssh $SSHTARGET <<_HERE
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
export TARGET=release
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
export CHATMAIL_DOMAIN=$CHATMAIL_DOMAIN
#we rely on tox/virtualenv being available in the host
#rm -rf virtualenv venv

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cargo install --path deltachat-rpc-server/ --root "$PWD/venv"
cargo install --path deltachat-rpc-server/ --root "$PWD/venv" --debug
PATH="$PWD/venv/bin:$PATH" tox -c deltachat-rpc-client

View File

@@ -27,7 +27,7 @@ mkdir -p $TOXWORKDIR
# XXX we may switch on some live-tests on for better ensurances
# Note that the independent remote_tests_python step does all kinds of
# live-testing already.
unset DCC_NEW_TMP_EMAIL
unset CHATMAIL_DOMAIN
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10

View File

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

View File

@@ -192,7 +192,7 @@ in different orders, esp. on creating new groups.
To remove a member, a `Chat-Group-Member-Removed` header must be sent
with the value set to the email-address of the member to remove.
When receiving a `Chat-Group-Member-Removed` header,
only exaxtly the given member has to be removed from the member list.
only exactly the given member has to be removed from the member list.
Messenger clients MUST NOT construct the member list
on other group messages
@@ -339,7 +339,7 @@ only on image changes.
In older specs, the profile-image was sent as an attachment
and `Chat-User-Avatar:` specified its name.
However, it turned out that these attachments are kind of unuexpected to users,
However, it turned out that these attachments are kind of unexpected to users,
therefore the profile-image go to the header now.

View File

@@ -258,8 +258,8 @@ impl Accounts {
}
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
pub async fn start_io(&self) {
for account in self.accounts.values() {
pub async fn start_io(&mut self) {
for account in self.accounts.values_mut() {
account.start_io().await;
}
}

View File

@@ -10,6 +10,7 @@ use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
@@ -20,7 +21,7 @@ use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
};
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
use crate::contact::{self, Contact, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
@@ -38,6 +39,7 @@ use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
use crate::stock_str;
use crate::sync::{self, ChatAction, Sync::*, SyncData};
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
@@ -206,9 +208,10 @@ impl ChatId {
self == DC_CHAT_ID_ALLDONE_HINT
}
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id`
/// if it exists and is not blocked.
///
/// If it does not exist, `None` is returned.
/// If the chat does not exist or is blocked, `None` is returned.
pub async fn lookup_by_contact(
context: &Context,
contact_id: ContactId,
@@ -225,7 +228,7 @@ impl ChatId {
/// This is an internal API, if **a user action** needs to get a chat
/// [`ChatId::create_for_contact`] should be used as this also scales up the
/// [`Contact`]'s origin.
pub async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
pub(crate) async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
.await
.map(|chat| chat.id)
@@ -250,7 +253,7 @@ impl ChatId {
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
Some(chat) => {
if create_blocked == Blocked::Not && chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
chat.id.set_blocked(context, Blocked::Not).await?;
}
chat.id
}
@@ -356,6 +359,10 @@ impl ChatId {
/// Blocks the chat as a result of explicit user action.
pub async fn block(self, context: &Context) -> Result<()> {
self.block_ex(context, Sync).await
}
pub(crate) async fn block_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
@@ -369,7 +376,7 @@ impl ChatId {
context,
"Blocking the contact {contact_id} to block 1:1 chat."
);
Contact::block(context, contact_id).await?;
contact::set_blocked(context, Nosync, contact_id, true).await?;
}
}
}
@@ -384,12 +391,28 @@ impl ChatId {
}
}
if sync.into() {
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
chat.add_sync_item(context, ChatAction::Block).await?;
}
Ok(())
}
/// Unblocks the chat.
pub async fn unblock(self, context: &Context) -> Result<()> {
self.unblock_ex(context, Sync).await
}
pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
self.set_blocked(context, Blocked::Not).await?;
if sync.into() {
let chat = Chat::load_from_db(context, self).await?;
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
// Maybe we should unblock the contact locally too, this would also resolve discrepancy
// with `block()` which also blocks the contact.
chat.add_sync_item(context, ChatAction::Unblock).await?;
}
Ok(())
}
@@ -397,6 +420,10 @@ impl ChatId {
///
/// Unblocks the chat and scales up origin of contacts.
pub async fn accept(self, context: &Context) -> Result<()> {
self.accept_ex(context, Sync).await
}
pub(crate) async fn accept_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
@@ -431,6 +458,9 @@ impl ChatId {
context.emit_event(EventType::ChatModified(self));
}
if sync.into() {
chat.add_sync_item(context, ChatAction::Accept).await?;
}
Ok(())
}
@@ -533,6 +563,15 @@ impl ChatId {
/// Archives or unarchives a chat.
pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
self.set_visibility_ex(context, Sync, visibility).await
}
pub(crate) async fn set_visibility_ex(
self,
context: &Context,
sync: sync::Sync,
visibility: ChatVisibility,
) -> Result<()> {
ensure!(
!self.is_special(),
"bad chat_id, can not be special chat: {}",
@@ -558,6 +597,11 @@ impl ChatId {
context.emit_msgs_changed_without_ids();
if sync.into() {
let chat = Chat::load_from_db(context, self).await?;
chat.add_sync_item(context, ChatAction::SetVisibility(visibility))
.await?;
}
Ok(())
}
@@ -630,7 +674,6 @@ impl ChatId {
"bad chat_id, can not be a special chat: {}",
self
);
/* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */
let chat = Chat::load_from_db(context, self).await?;
context
@@ -1209,6 +1252,16 @@ impl ChatId {
Ok(())
}
/// Returns true if the chat is protected.
pub async fn is_protected(self, context: &Context) -> Result<ProtectionStatus> {
let protection_status = context
.sql
.query_get_value("SELECT protected FROM chats WHERE id=?", (self,))
.await?
.unwrap_or_default();
Ok(protection_status)
}
}
impl std::fmt::Display for ChatId {
@@ -1270,7 +1323,8 @@ pub struct Chat {
/// Whether the chat is archived or pinned.
pub visibility: ChatVisibility,
/// Group ID.
/// Group ID. For [`Chattype::Mailinglist`] -- mailing list address. Empty for 1:1 chats and
/// ad-hoc groups.
pub grpid: String,
/// Whether the chat is blocked, unblocked or a contact request.
@@ -1541,6 +1595,15 @@ impl Chat {
}
/// Returns true if chat protection is enabled.
///
/// 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.
pub fn is_protected(&self) -> bool {
self.protected == ProtectionStatus::Protected
}
@@ -1827,29 +1890,62 @@ impl Chat {
context.scheduler.interrupt_ephemeral_task().await;
Ok(msg.id)
}
/// Returns chat id for the purpose of synchronisation across devices.
async fn get_sync_id(&self, context: &Context) -> Result<Option<sync::ChatId>> {
match self.typ {
Chattype::Single => {
let mut r = None;
for contact_id in get_chat_contacts(context, self.id).await? {
if contact_id == ContactId::SELF && !self.is_self_talk() {
continue;
}
if r.is_some() {
return Ok(None);
}
let contact = Contact::get_by_id(context, contact_id).await?;
r = Some(sync::ChatId::ContactAddr(contact.get_addr().to_string()));
}
Ok(r)
}
Chattype::Broadcast | Chattype::Group | Chattype::Mailinglist => {
if self.grpid.is_empty() {
return Ok(None);
}
Ok(Some(sync::ChatId::Grpid(self.grpid.clone())))
}
}
}
/// Adds a chat action to the list of items to synchronise to other devices.
pub(crate) async fn add_sync_item(&self, context: &Context, action: ChatAction) -> Result<()> {
if let Some(id) = self.get_sync_id(context).await? {
context
.add_sync_item(SyncData::AlterChat { id, action })
.await?;
context.send_sync_msg().await?;
}
Ok(())
}
}
/// Whether the chat is pinned or archived.
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize)]
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
#[repr(i8)]
pub enum ChatVisibility {
/// Chat is neither archived nor pinned.
Normal,
Normal = 0,
/// Chat is archived.
Archived,
Archived = 1,
/// Chat is pinned to the top of the chatlist.
Pinned,
Pinned = 2,
}
impl rusqlite::types::ToSql for ChatVisibility {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let visibility = match &self {
ChatVisibility::Normal => 0,
ChatVisibility::Archived => 1,
ChatVisibility::Pinned => 2,
};
let val = rusqlite::types::Value::Integer(visibility);
let val = rusqlite::types::Value::Integer(*self as i64);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -3060,6 +3156,13 @@ pub async fn create_group_chat(
.await?;
}
if !context.get_config_bool(Config::Bot).await?
&& !context.get_config_bool(Config::SkipStartMessages).await?
{
let text = stock_str::new_group_send_first_message(context).await;
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
}
Ok(chat_id)
}
@@ -3280,7 +3383,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
}
/// Chat mute duration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MuteDuration {
/// Chat is not muted.
NotMuted,
@@ -3329,6 +3432,15 @@ impl rusqlite::types::FromSql for MuteDuration {
/// Mutes the chat for a given duration or unmutes it.
pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<()> {
set_muted_ex(context, Sync, chat_id, duration).await
}
pub(crate) async fn set_muted_ex(
context: &Context,
sync: sync::Sync,
chat_id: ChatId,
duration: MuteDuration,
) -> Result<()> {
ensure!(!chat_id.is_special(), "Invalid chat ID");
context
.sql
@@ -3339,6 +3451,11 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio
.await
.context(format!("Failed to set mute duration for {chat_id}"))?;
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
let chat = Chat::load_from_db(context, chat_id).await?;
chat.add_sync_item(context, ChatAction::SetMuted(duration))
.await?;
}
Ok(())
}
@@ -3963,6 +4080,56 @@ pub(crate) async fn update_msg_text_and_timestamp(
Ok(())
}
impl Context {
/// Executes [`SyncData::AlterChat`] item sent by other device.
pub(crate) async fn sync_alter_chat(
&self,
id: &sync::ChatId,
action: &ChatAction,
) -> Result<()> {
let chat_id = match id {
sync::ChatId::ContactAddr(addr) => {
let Some(contact_id) =
Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None).await?
else {
warn!(self, "sync_alter_chat: No contact for addr '{addr}'.");
return Ok(());
};
match action {
ChatAction::Block => {
return contact::set_blocked(self, Nosync, contact_id, true).await
}
ChatAction::Unblock => {
return contact::set_blocked(self, Nosync, contact_id, false).await
}
_ => (),
}
let Some(chat_id) = ChatId::lookup_by_contact(self, contact_id).await? else {
warn!(self, "sync_alter_chat: No chat for addr '{addr}'.");
return Ok(());
};
chat_id
}
sync::ChatId::Grpid(grpid) => {
let Some((chat_id, ..)) = get_chat_id_by_grpid(self, grpid).await? else {
warn!(self, "sync_alter_chat: No chat for grpid '{grpid}'.");
return Ok(());
};
chat_id
}
};
match action {
ChatAction::Block => chat_id.block_ex(self, Nosync).await,
ChatAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
ChatAction::Accept => chat_id.accept_ex(self, Nosync).await,
ChatAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
ChatAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
}
.ok();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -4306,13 +4473,11 @@ mod tests {
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
// Bob receives a msg about Alice adding Claire to the group.
let bob_received_add_msg = bob.recv_msg(&alice_sent_add_msg).await;
bob.recv_msg(&alice_sent_add_msg).await;
// Test that add message is rewritten.
assert_eq!(
bob_received_add_msg.get_text(),
"Member claire@example.net added by alice@example.org."
);
bob.golden_test_chat(bob_chat_id, "chat_test_simultaneous_member_remove")
.await;
// Bob receives a msg about Alice removing him from the group.
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;

View File

@@ -266,6 +266,10 @@ pub enum Config {
/// True if it is a bot account.
Bot,
/// True when to skip initial start messages in groups.
#[strum(props(default = "0"))]
SkipStartMessages,
/// Whether we send a warning if the password is wrong (set to false when we send a warning
/// because we do not want to send a second warning)
#[strum(props(default = "0"))]
@@ -298,7 +302,7 @@ pub enum Config {
DownloadLimit,
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set.
#[strum(props(default = "0"))]
#[strum(props(default = "1"))]
SyncMsgs,
/// Space-separated list of all the authserv-ids which we believe

View File

@@ -19,7 +19,7 @@ use tokio::task;
use tokio::time::{timeout, Duration};
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
use crate::chat::{ChatId, ProtectionStatus};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
@@ -32,6 +32,7 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::sql::{self, params_iter};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
EmailAddress,
@@ -459,12 +460,12 @@ impl Contact {
/// Block the given contact.
pub async fn block(context: &Context, id: ContactId) -> Result<()> {
set_block_contact(context, id, true).await
set_blocked(context, Sync, id, true).await
}
/// Unblock the given contact.
pub async fn unblock(context: &Context, id: ContactId) -> Result<()> {
set_block_contact(context, id, false).await
set_blocked(context, Sync, id, false).await
}
/// Add a single contact as a result of an _explicit_ user action.
@@ -494,7 +495,7 @@ impl Contact {
}
}
if blocked {
Contact::unblock(context, contact_id).await?;
set_blocked(context, Nosync, contact_id, false).await?;
}
Ok(contact_id)
@@ -523,10 +524,24 @@ impl Contact {
///
/// To validate an e-mail address independently of the contact database
/// use `may_be_valid_addr()`.
///
/// Returns the contact ID of the contact belonging to the e-mail address or 0 if there is no
/// contact that is or was introduced by an accepted contact.
pub async fn lookup_id_by_addr(
context: &Context,
addr: &str,
min_origin: Origin,
) -> Result<Option<ContactId>> {
Self::lookup_id_by_addr_ex(context, addr, min_origin, Some(Blocked::Not)).await
}
/// The same as `lookup_id_by_addr()`, but internal function. Currently also allows looking up
/// not unblocked contacts.
pub(crate) async fn lookup_id_by_addr_ex(
context: &Context,
addr: &str,
min_origin: Origin,
blocked: Option<Blocked>,
) -> Result<Option<ContactId>> {
if addr.is_empty() {
bail!("lookup_id_by_addr: empty address");
@@ -543,8 +558,14 @@ impl Contact {
.query_get_value(
"SELECT id FROM contacts \
WHERE addr=?1 COLLATE NOCASE \
AND id>?2 AND origin>=?3 AND blocked=0;",
(&addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32),
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
(
&addr_normalized,
ContactId::LAST_SPECIAL,
min_origin as u32,
blocked.is_none(),
blocked.unwrap_or_default(),
),
)
.await?;
Ok(id)
@@ -1230,10 +1251,21 @@ impl Contact {
self.status.as_str()
}
/// Check if a contact was verified. E.g. by a secure-join QR code scan
/// and if the key has not changed since this verification.
/// Returns true if the contact
/// can be added to verified chats,
/// i.e. has a verified key
/// and Autocrypt key matches the verified key.
///
/// The UI may draw a checkbox or something like that beside verified contacts.
/// If contact is verified
/// UI should display green checkmark after the contact name
/// in contact list items and
/// in chat member list items.
///
/// Do not use this function when displaying the contact profile view.
/// Display green checkmark in the title of the contact profile
/// if [Self::is_profile_verified] returns true.
/// Use [Self::get_verifier_id] to display the verifier contact
/// in the info section of the contact profile.
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
@@ -1250,16 +1282,23 @@ impl Contact {
Ok(VerifiedStatus::Unverified)
}
/// Returns the address that verified the contact.
pub async fn get_verifier_addr(&self, context: &Context) -> Result<Option<String>> {
Ok(Peerstate::from_addr(context, self.get_addr())
.await?
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned())))
}
/// Returns the ContactId that verified the contact.
/// Returns the `ContactId` that verified the 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 [Self::get_name_n_addr].
///
/// If this function returns a verifier,
/// this does not necessarily mean
/// you can add the contact to verified chats.
/// Use [Self::is_verified] to check
/// if a contact can be added to a verified chat instead.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
let Some(verifier_addr) = Peerstate::from_addr(context, self.get_addr())
.await?
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))
else {
return Ok(None);
};
@@ -1278,6 +1317,22 @@ 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.
pub async fn is_profile_verified(&self, context: &Context) -> Result<bool> {
let contact_id = self.id;
if let Some(chat_id) = ChatId::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? == VerifiedStatus::BidirectVerified)
}
}
/// 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 {
@@ -1363,8 +1418,9 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
}
}
async fn set_block_contact(
pub(crate) async fn set_blocked(
context: &Context,
sync: sync::Sync,
contact_id: ContactId,
new_blocking: bool,
) -> Result<()> {
@@ -1373,7 +1429,6 @@ async fn set_block_contact(
"Can't block special contact {}",
contact_id
);
let contact = Contact::get_by_id(context, contact_id).await?;
if contact.blocked != new_blocking {
@@ -1415,9 +1470,23 @@ WHERE type=? AND id IN (
if let Some((chat_id, _, _)) =
chat::get_chat_id_by_grpid(context, &contact.addr).await?
{
chat_id.unblock(context).await?;
chat_id.unblock_ex(context, Nosync).await?;
}
}
if sync.into() {
let action = match new_blocking {
true => sync::ChatAction::Block,
false => sync::ChatAction::Unblock,
};
context
.add_sync_item(SyncData::AlterChat {
id: sync::ChatId::ContactAddr(contact.addr.clone()),
action,
})
.await?;
context.send_sync_msg().await?;
}
}
Ok(())
@@ -2710,7 +2779,6 @@ Hi."#;
let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_addr(&alice).await?.is_none());
assert!(contact.get_verifier_id(&alice).await?.is_none());
// Receive a message from Bob to create a peerstate.
@@ -2719,7 +2787,6 @@ Hi."#;
alice.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_addr(&alice).await?.is_none());
assert!(contact.get_verifier_id(&alice).await?.is_none());
Ok(())

View File

@@ -398,11 +398,24 @@ impl Context {
}
/// Starts the IO scheduler.
pub async fn start_io(&self) {
if let Ok(false) = self.is_configured().await {
pub async fn start_io(&mut self) {
if !self.is_configured().await.unwrap_or_default() {
warn!(self, "can not start io on a context that is not configured");
return;
}
{
if self
.get_config(Config::ConfiguredAddr)
.await
.unwrap_or_default()
.filter(|s| s.ends_with(".testrun.org"))
.is_some()
{
let mut lock = self.ratelimit.write().await;
*lock = Ratelimit::new(Duration::new(40, 0), 6.0);
}
}
self.scheduler.start(self.clone()).await;
}
@@ -1302,6 +1315,7 @@ mod tests {
"send_port",
"send_security",
"server_flags",
"skip_start_messages",
"smtp_certificate_checks",
"socks5_host",
"socks5_port",

View File

@@ -18,12 +18,11 @@ use crate::{stock_str, EventType};
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
/// Some messages as non-delivery-reports (NDN) or read-receipts (MDN)
/// need to be downloaded completely to handle them correctly,
/// eg. to assign them to the correct chat.
/// As these messages are typically small,
/// they're caught by `MIN_DOWNLOAD_LIMIT`.
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
/// For better UX, some messages as add-member, non-delivery-reports (NDN) or read-receipts (MDN)
/// should always be downloaded completely to handle them correctly,
/// also in larger groups and if group and contact avatar are attached.
/// Most of these cases are caught by `MIN_DOWNLOAD_LIMIT`.
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 163840;
/// If a message is downloaded only partially
/// and `delete_server_after` is set to small timeouts (eg. "at once"),

View File

@@ -67,13 +67,8 @@ impl EncryptHelper {
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
);
match peerstate.prefer_encrypt {
EncryptPreference::NoPreference => {}
EncryptPreference::NoPreference | EncryptPreference::Reset => {}
EncryptPreference::Mutual => prefer_encrypt_count += 1,
EncryptPreference::Reset => {
if !e2ee_guaranteed {
return Ok(false);
}
}
};
}
None => {

View File

@@ -250,6 +250,7 @@ pub enum EventType {
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 1000=vg-member-added/vc-contact-confirm received
progress: usize,
},

View File

@@ -568,9 +568,14 @@ impl Imap {
Ok(())
}
/// Select a folder and take care of uidvalidity changes.
/// Also, when selecting a folder for the first time, sets the uid_next to the current
/// Selects a folder and takes care of UIDVALIDITY changes.
///
/// When selecting a folder for the first time, sets the uid_next to the current
/// mailbox.uid_next so that no old emails are fetched.
///
/// Makes sure that UIDNEXT is known for `selected_mailbox`
/// and errors out if UIDNEXT cannot be determined.
///
/// Returns Result<new_emails> (i.e. whether new emails arrived),
/// if in doubt, returns new_emails=true so emails are fetched.
pub(crate) async fn select_with_uidvalidity(
@@ -591,6 +596,37 @@ impl Imap {
let new_uid_validity = mailbox
.uid_validity
.with_context(|| format!("No UIDVALIDITY for folder {folder}"))?;
let new_uid_next = if let Some(uid_next) = mailbox.uid_next {
uid_next
} else {
warn!(
context,
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
);
// RFC 3501 says STATUS command SHOULD NOT be used
// on the currently selected mailbox because the same
// information can be obtained by other means,
// such as reading SELECT response.
//
// However, it also says that UIDNEXT is REQUIRED
// in the SELECT response and if we are here,
// it is actually not returned.
//
// In particular, Winmail Pro Mail Server 5.1.0616
// never returns UIDNEXT in SELECT response,
// but responds to "STATUS INBOX (UIDNEXT)" command.
let status = session
.inner
.status(folder, "(UIDNEXT)")
.await
.context("STATUS (UIDNEXT) error for {folder:?}")?;
status
.uid_next
.context("STATUS {folder} (UIDNEXT) did not return UIDNEXT")?
};
mailbox.uid_next = Some(new_uid_next);
let old_uid_validity = get_uidvalidity(context, folder)
.await
@@ -606,18 +642,16 @@ impl Imap {
// the caller tries to fetch new messages (we could of course run a SELECT command now, but trying to fetch
// new messages is only one command, just as a SELECT command)
true
} else if let Some(uid_next) = mailbox.uid_next {
if uid_next < old_uid_next {
} else {
if new_uid_next < old_uid_next {
warn!(
context,
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
);
set_uid_next(context, folder, uid_next).await?;
set_uid_next(context, folder, new_uid_next).await?;
context.schedule_resync().await?;
}
uid_next != old_uid_next // If uid_next changed, there are new emails
} else {
true // We have no uid_next and if in doubt, return true
new_uid_next != old_uid_next // If UIDNEXT changed, there are new emails
};
return Ok(new_emails);
}
@@ -627,43 +661,6 @@ impl Imap {
// ============== uid_validity has changed or is being set the first time. ==============
let new_uid_next = match mailbox.uid_next {
Some(uid_next) => uid_next,
None => {
warn!(
context,
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
);
// RFC 3501 says STATUS command SHOULD NOT be used
// on the currently selected mailbox because the same
// information can be obtained by other means,
// such as reading SELECT response.
//
// However, it also says that UIDNEXT is REQUIRED
// in the SELECT response and if we are here,
// it is actually not returned.
//
// In particular, Winmail Pro Mail Server 5.1.0616
// never returns UIDNEXT in SELECT response,
// but responds to "SELECT INBOX (UIDNEXT)" command.
let status = session
.inner
.status(folder, "(UIDNEXT)")
.await
.context("STATUS (UIDNEXT) error for {folder:?}")?;
if let Some(uid_next) = status.uid_next {
uid_next
} else {
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
// Set UIDNEXT to 1 as a last resort fallback.
1
}
}
};
set_uid_next(context, folder, new_uid_next).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
@@ -867,14 +864,28 @@ impl Imap {
uids_fetch_in_batch.push(uid);
}
// determine which uid_next to use to update to
// receive_imf() returns an `Err` value only on recoverable errors, otherwise it just logs an error.
// `largest_uid_processed` is the largest uid where receive_imf() did NOT return an error.
// So: Update the uid_next to the largest uid that did NOT recoverably fail. Not perfect because if there was
// another message afterwards that succeeded, we will not retry. The upside is that we will not retry an infinite amount of times.
let largest_uid_without_errors = max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0));
let new_uid_next = largest_uid_without_errors + 1;
// Advance uid_next to the maximum of the largest known UID plus 1
// and mailbox UIDNEXT.
// Largest known UID is normally less than UIDNEXT,
// but a message may have arrived between determining UIDNEXT
// and executing the FETCH command.
let mailbox_uid_next = self
.session
.as_ref()
.context("No IMAP session")?
.selected_mailbox
.as_ref()
.with_context(|| format!("Expected {folder:?} to be selected"))?
.uid_next
.with_context(|| {
format!(
"Expected UIDNEXT to be determined for {folder:?} by select_with_uidvalidity"
)
})?;
let new_uid_next = max(
max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
mailbox_uid_next,
);
if new_uid_next > old_uid_next {
set_uid_next(context, folder, new_uid_next).await?;

View File

@@ -8,7 +8,7 @@ use futures_lite::FutureExt;
use super::session::Session;
use super::Imap;
use crate::config::Config;
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::imap::{client::IMAP_TIMEOUT, get_uid_next, FolderMeaning};
use crate::log::LogExt;
use crate::{context::Context, scheduler::InterruptInfo};
@@ -19,7 +19,7 @@ impl Session {
mut self,
context: &Context,
idle_interrupt_receiver: Receiver<InterruptInfo>,
watch_folder: Option<String>,
folder: &str,
) -> Result<(Self, InterruptInfo)> {
use futures::future::FutureExt;
@@ -33,12 +33,35 @@ impl Session {
let mut info = Default::default();
self.select_folder(context, watch_folder.as_deref()).await?;
self.select_folder(context, Some(folder)).await?;
if self.server_sent_unsolicited_exists(context)? {
return Ok((self, info));
}
// Despite checking for unsolicited EXISTS above,
// we may have missed EXISTS if the message was
// received when the folder was not selected.
let status = self
.status(folder, "(UIDNEXT)")
.await
.context("STATUS (UIDNEXT) error for {folder:?}")?;
if let Some(uid_next) = status.uid_next {
let expected_uid_next = get_uid_next(context, folder)
.await
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
if uid_next > expected_uid_next {
info!(
context,
"Skipping IDLE on {folder:?} because UIDNEXT {uid_next}>{expected_uid_next} indicates there are new messages."
);
return Ok((self, info));
}
} else {
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
// Go to IDLE anyway if STATUS is broken.
}
if let Ok(info) = idle_interrupt_receiver.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
return Ok((self, info));
@@ -61,11 +84,7 @@ impl Session {
Interrupt(InterruptInfo),
}
let folder_name = watch_folder.as_deref().unwrap_or("None");
info!(
context,
"{}: Idle entering wait-on-remote state", folder_name
);
info!(context, "{folder}: Idle entering wait-on-remote state");
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = idle_interrupt_receiver.recv().await;
@@ -77,36 +96,27 @@ impl Session {
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
info!(context, "{folder}: Idle has NewData {:?}", x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(
context,
"{}: Idle-wait timeout or interruption", folder_name
);
info!(context, "{folder}: Idle-wait timeout or interruption");
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(
context,
"{}: Idle wait was interrupted manually", folder_name
);
info!(context, "{folder}: Idle wait was interrupted manually");
}
Ok(Event::Interrupt(i)) => {
info!(
context,
"{}: Idle wait was interrupted: {:?}", folder_name, &i
);
info!(context, "{folder}: Idle wait was interrupted: {:?}", &i);
info = i;
}
Err(err) => {
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
warn!(context, "{folder}: Idle wait errored: {err:?}");
}
}
let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done())
.await
.with_context(|| format!("{folder_name}: IMAP IDLE protocol timed out"))?
.with_context(|| format!("{folder_name}: IMAP IDLE failed"))?;
.with_context(|| format!("{folder}: IMAP IDLE protocol timed out"))?
.with_context(|| format!("{folder}: IMAP IDLE failed"))?;
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
self.inner = session;

View File

@@ -150,21 +150,53 @@ WHERE id=?;
self.0
}
/// Returns raw text of a message, used for message info
pub async fn rawtext(self, context: &Context) -> Result<String> {
Ok(context
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?", (self,))
.await?
.unwrap_or_default())
}
/// Returns server foldernames and UIDs of a message, used for message info
pub async fn get_info_server_urls(
context: &Context,
rfc724_mid: String,
) -> Result<Vec<String>> {
context
.sql
.query_map(
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
(rfc724_mid,),
|row| {
let folder: String = row.get("folder")?;
let uid: u32 = row.get("uid")?;
Ok(format!("</{folder}/;UID={uid}>"))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await
}
/// Returns information about hops of a message, used for message info
pub async fn hop_info(self, context: &Context) -> Result<Option<String>> {
context
.sql
.query_get_value("SELECT hop_info FROM msgs WHERE id=?", (self,))
.await
}
/// Returns detailed message information in a multi-line text form.
pub async fn get_info(self, context: &Context) -> Result<String> {
let msg = Message::load_from_db(context, self).await?;
let rawtxt: Option<String> = context
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?", (self,))
.await?;
let rawtxt: String = self.rawtext(context).await?;
let mut ret = String::new();
if rawtxt.is_none() {
ret += &format!("Cannot load message {self}.");
return Ok(ret);
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
let fts = timestamp_to_str(msg.get_timestamp());
@@ -282,32 +314,13 @@ WHERE id=?;
if !msg.rfc724_mid.is_empty() {
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
let server_uids = context
.sql
.query_map(
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
(msg.rfc724_mid,),
|row| {
let folder: String = row.get("folder")?;
let uid: u32 = row.get("uid")?;
Ok((folder, uid))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for (folder, uid) in server_uids {
let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
for server_url in server_urls {
// Format as RFC 5092 relative IMAP URL.
ret += &format!("\n</{folder}/;UID={uid}>");
ret += &format!("\n{server_url}");
}
}
let hop_info: Option<String> = context
.sql
.query_get_value("SELECT hop_info FROM msgs WHERE id=?;", (self,))
.await?;
let hop_info = self.hop_info(context).await?;
ret += "\n\n";
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned());
@@ -649,6 +662,12 @@ impl Message {
self.id
}
/// Returns the rfc724 message ID
/// May be empty
pub fn rfc724_mid(&self) -> &str {
&self.rfc724_mid
}
/// Returns the ID of the contact who wrote the message.
pub fn get_from_id(&self) -> ContactId {
self.from_id
@@ -1799,6 +1818,14 @@ pub async fn estimate_deletion_cnt(
pub(crate) async fn rfc724_mid_exists(
context: &Context,
rfc724_mid: &str,
) -> Result<Option<MsgId>> {
rfc724_mid_exists_and(context, rfc724_mid, "1").await
}
pub(crate) async fn rfc724_mid_exists_and(
context: &Context,
rfc724_mid: &str,
cond: &str,
) -> Result<Option<MsgId>> {
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
if rfc724_mid.is_empty() {
@@ -1809,7 +1836,7 @@ pub(crate) async fn rfc724_mid_exists(
let res = context
.sql
.query_row_optional(
"SELECT id FROM msgs WHERE rfc724_mid=?",
&("SELECT id FROM msgs WHERE rfc724_mid=? AND ".to_string() + cond),
(rfc724_mid,),
|row| {
let msg_id: MsgId = row.get(0)?;

View File

@@ -59,7 +59,7 @@ pub(crate) struct MimeMessage {
/// Message headers.
headers: HashMap<String, String>,
/// Addresses are normalized and lowercased:
/// Addresses are normalized and lowercase
pub recipients: Vec<SingleInfo>,
/// `From:` address.
@@ -376,6 +376,12 @@ impl MimeMessage {
if !encrypted {
signatures.clear();
}
if let Some(peerstate) = &mut decryption_info.peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await?;
}
}
// Auto-submitted is also set by holiday-notices so we also check `chat-version`
let is_bot = headers.contains_key("auto-submitted") && headers.contains_key("chat-version");
@@ -1358,7 +1364,7 @@ impl MimeMessage {
self.get_mailinglist_header().is_some()
}
pub fn repl_msg_by_error(&mut self, error_msg: &str) {
pub fn replace_msg_by_error(&mut self, error_msg: &str) {
self.is_system_message = SystemMessage::Unknown;
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::Text;

View File

@@ -1,10 +1,11 @@
//! # Common network utilities.
use std::net::Ipv4Addr;
use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as _, Error, Result};
use anyhow::{format_err, Context as _, Result};
use tokio::net::{lookup_host, TcpStream};
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
@@ -119,6 +120,22 @@ async fn lookup_host_with_cache(
}
}
}
if resolved_addrs.is_empty() {
// Load hardcoded cache if everything else fails.
//
// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
//
// In the future we may pre-resolve all provider database addresses
// and build them in.
if hostname == "mail.sangham.net" {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
port,
));
}
}
}
Ok(resolved_addrs)
@@ -178,7 +195,9 @@ pub(crate) async fn connect_tcp(
let tcp_stream = match tcp_stream {
Some(tcp_stream) => tcp_stream,
None => {
return Err(last_error.unwrap_or_else(|| Error::msg("no DNS resolution results")));
return Err(
last_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}"))
);
}
};

View File

@@ -427,42 +427,37 @@ impl Peerstate {
&mut self,
which_key: PeerstateKeyType,
fingerprint: Fingerprint,
verified: PeerstateVerifiedStatus,
verifier: String,
) -> Result<()> {
if verified == PeerstateVerifiedStatus::BidirectVerified {
match which_key {
PeerstateKeyType::PublicKey => {
if self.public_key_fingerprint.is_some()
&& self.public_key_fingerprint.as_ref().unwrap() == &fingerprint
{
self.verified_key = self.public_key.clone();
self.verified_key_fingerprint = Some(fingerprint);
self.verifier = Some(verifier);
Ok(())
} else {
Err(Error::msg(format!(
"{fingerprint} is not peer's public key fingerprint",
)))
}
}
PeerstateKeyType::GossipKey => {
if self.gossip_key_fingerprint.is_some()
&& self.gossip_key_fingerprint.as_ref().unwrap() == &fingerprint
{
self.verified_key = self.gossip_key.clone();
self.verified_key_fingerprint = Some(fingerprint);
self.verifier = Some(verifier);
Ok(())
} else {
Err(Error::msg(format!(
"{fingerprint} is not peer's gossip key fingerprint",
)))
}
match which_key {
PeerstateKeyType::PublicKey => {
if self.public_key_fingerprint.is_some()
&& self.public_key_fingerprint.as_ref().unwrap() == &fingerprint
{
self.verified_key = self.public_key.clone();
self.verified_key_fingerprint = Some(fingerprint);
self.verifier = Some(verifier);
Ok(())
} else {
Err(Error::msg(format!(
"{fingerprint} is not peer's public key fingerprint",
)))
}
}
PeerstateKeyType::GossipKey => {
if self.gossip_key_fingerprint.is_some()
&& self.gossip_key_fingerprint.as_ref().unwrap() == &fingerprint
{
self.verified_key = self.gossip_key.clone();
self.verified_key_fingerprint = Some(fingerprint);
self.verifier = Some(verifier);
Ok(())
} else {
Err(Error::msg(format!(
"{fingerprint} is not peer's gossip key fingerprint",
)))
}
}
} else {
Err(Error::msg("BidirectVerified required"))
}
}

View File

@@ -222,6 +222,99 @@ static P_BUZON_UY: Provider = Provider {
oauth2_authorizer: None,
};
// c1.testrun.org.md: c1.testrun.org
static P_C1_TESTRUN_ORG: Provider = Provider {
id: "c1.testrun.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/c1-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "c1.testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "c1.testrun.org",
port: 465,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::MvboxMove,
value: "0",
}]),
oauth2_authorizer: None,
};
// c2.testrun.org.md: c2.testrun.org
static P_C2_TESTRUN_ORG: Provider = Provider {
id: "c2.testrun.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/c2-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "c2.testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "c2.testrun.org",
port: 465,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::MvboxMove,
value: "0",
}]),
oauth2_authorizer: None,
};
// c3.testrun.org.md: c3.testrun.org
static P_C3_TESTRUN_ORG: Provider = Provider {
id: "c3.testrun.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/c3-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "c3.testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "c3.testrun.org",
port: 465,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::MvboxMove,
value: "0",
}]),
oauth2_authorizer: None,
};
// chello.at.md: chello.at
static P_CHELLO_AT: Provider = Provider {
id: "chello.at",
@@ -867,6 +960,37 @@ static P_NAVER: Provider = Provider {
oauth2_authorizer: None,
};
// nine.testrun.org.md: nine.testrun.org
static P_NINE_TESTRUN_ORG: Provider = Provider {
id: "nine.testrun.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/nine-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "nine.testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "nine.testrun.org",
port: 465,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::MvboxMove,
value: "0",
}]),
oauth2_authorizer: None,
};
// nubo.coop.md: nubo.coop
static P_NUBO_COOP: Provider = Provider {
id: "nubo.coop",
@@ -1495,6 +1619,9 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("delta.blindzeln.org", &P_BLINDZELN_ORG),
("bluewin.ch", &P_BLUEWIN_CH),
("buzon.uy", &P_BUZON_UY),
("c1.testrun.org", &P_C1_TESTRUN_ORG),
("c2.testrun.org", &P_C2_TESTRUN_ORG),
("c3.testrun.org", &P_C3_TESTRUN_ORG),
("chello.at", &P_CHELLO_AT),
("xfinity.com", &P_COMCAST),
("comcast.net", &P_COMCAST),
@@ -1708,6 +1835,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("mailo.com", &P_MAILO_COM),
("nauta.cu", &P_NAUTA_CU),
("naver.com", &P_NAVER),
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
("nubo.coop", &P_NUBO_COOP),
("hotmail.com", &P_OUTLOOK_COM),
("outlook.com", &P_OUTLOOK_COM),
@@ -1860,6 +1988,9 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("blindzeln.org", &P_BLINDZELN_ORG),
("bluewin.ch", &P_BLUEWIN_CH),
("buzon.uy", &P_BUZON_UY),
("c1.testrun.org", &P_C1_TESTRUN_ORG),
("c2.testrun.org", &P_C2_TESTRUN_ORG),
("c3.testrun.org", &P_C3_TESTRUN_ORG),
("chello.at", &P_CHELLO_AT),
("comcast", &P_COMCAST),
("dismail.de", &P_DISMAIL_DE),
@@ -1888,6 +2019,7 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("mailo.com", &P_MAILO_COM),
("nauta.cu", &P_NAUTA_CU),
("naver", &P_NAVER),
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
("nubo.coop", &P_NUBO_COOP),
("outlook.com", &P_OUTLOOK_COM),
("ouvaton.coop", &P_OUVATON_COOP),
@@ -1918,4 +2050,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 2, 20).unwrap());
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 11, 5).unwrap());

View File

@@ -26,16 +26,18 @@ use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
use crate::location;
use crate::log::LogExt;
use crate::message::{
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
self, rfc724_mid_exists, rfc724_mid_exists_and, Message, MessageState, MessengerMessage, MsgId,
Viewtype,
};
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::peerstate::{Peerstate, PeerstateKeyType};
use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::simplify;
use crate::sql;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{
buf_compress, extract_grpid_from_rfc724_mid, smeared_time, strip_rtlo_characters,
};
@@ -455,6 +457,7 @@ async fn add_parts(
let mut chat_id_blocked = Blocked::Not;
let mut better_msg = None;
let mut group_changes_msgs = Vec::new();
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
}
@@ -502,7 +505,6 @@ async fn add_parts(
// - incoming messages introduce a chat only for known contacts if they are sent by a messenger
// (of course, the user can add other chats manually later)
let to_id: ContactId;
let state: MessageState;
let mut needs_delete_job = false;
if incoming {
@@ -514,25 +516,23 @@ async fn add_parts(
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
match handle_securejoin_handshake(context, mime_parser, from_id).await {
Ok(securejoin::HandshakeMessage::Done) => {
match handle_securejoin_handshake(context, mime_parser, from_id)
.await
.context("error in Secure-Join message handling")?
{
securejoin::HandshakeMessage::Done => {
chat_id = Some(DC_CHAT_ID_TRASH);
needs_delete_job = true;
securejoin_seen = true;
}
Ok(securejoin::HandshakeMessage::Ignore) => {
securejoin::HandshakeMessage::Ignore => {
chat_id = Some(DC_CHAT_ID_TRASH);
securejoin_seen = true;
}
Ok(securejoin::HandshakeMessage::Propagate) => {
securejoin::HandshakeMessage::Propagate => {
// process messages as "member added" normally
securejoin_seen = false;
}
Err(err) => {
warn!(context, "Error in Secure-Join message handling: {err:#}.");
chat_id = Some(DC_CHAT_ID_TRASH);
securejoin_seen = true;
}
}
// Peerstate could be updated by handling the Securejoin handshake.
let contact = Contact::get_by_id(context, from_id).await?;
@@ -637,7 +637,7 @@ async fn add_parts(
chat_id = None;
} else {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(&s);
mime_parser.replace_msg_by_error(&s);
}
} else {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
@@ -650,15 +650,16 @@ async fn add_parts(
}
}
better_msg = better_msg.or(apply_group_changes(
group_changes_msgs = apply_group_changes(
context,
mime_parser,
sent_timestamp,
group_chat_id,
from_id,
to_ids,
is_partial_download.is_some(),
)
.await?);
.await?;
}
if chat_id.is_none() {
@@ -808,19 +809,17 @@ async fn add_parts(
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
match observe_securejoin_on_other_device(context, mime_parser, to_id).await {
Ok(securejoin::HandshakeMessage::Done)
| Ok(securejoin::HandshakeMessage::Ignore) => {
match observe_securejoin_on_other_device(context, mime_parser, to_id)
.await
.context("error in Secure-Join watching")?
{
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
chat_id = Some(DC_CHAT_ID_TRASH);
}
Ok(securejoin::HandshakeMessage::Propagate) => {
securejoin::HandshakeMessage::Propagate => {
// process messages as "member added" normally
chat_id = None;
}
Err(err) => {
warn!(context, "Error in Secure-Join watching: {err:#}.");
chat_id = Some(DC_CHAT_ID_TRASH);
}
}
} else if mime_parser.sync_items.is_some() && self_sent {
chat_id = Some(DC_CHAT_ID_TRASH);
@@ -887,22 +886,23 @@ async fn add_parts(
// automatically unblock chat when the user sends a message
if chat_id_blocked != Blocked::Not {
if let Some(chat_id) = chat_id {
chat_id.unblock(context).await?;
chat_id.unblock_ex(context, Nosync).await?;
chat_id_blocked = Blocked::Not;
}
}
}
if let Some(chat_id) = chat_id {
better_msg = better_msg.or(apply_group_changes(
group_changes_msgs = apply_group_changes(
context,
mime_parser,
sent_timestamp,
chat_id,
from_id,
to_ids,
is_partial_download.is_some(),
)
.await?);
.await?;
}
if chat_id.is_none() && self_sent {
@@ -919,7 +919,7 @@ async fn add_parts(
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked {
chat_id.unblock(context).await?;
chat_id.unblock_ex(context, Nosync).await?;
// Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
}
}
@@ -1055,7 +1055,7 @@ async fn add_parts(
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
mime_parser.replace_msg_by_error(&s);
}
}
}
@@ -1116,7 +1116,9 @@ async fn add_parts(
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
for part in &mut mime_parser.parts {
group_changes_msgs = group_changes_msgs.into_iter().rev().collect();
let mut parts = mime_parser.parts.iter_mut().peekable();
while let Some(part) = parts.peek() {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
set_msg_reaction(
@@ -1149,7 +1151,10 @@ async fn add_parts(
}
let mut txt_raw = "".to_string();
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
let group_changes_msg = group_changes_msgs.pop();
let (msg, typ): (&str, Viewtype) = if let Some(msg) = &group_changes_msg {
(msg, Viewtype::Text)
} else if let Some(better_msg) = &better_msg {
(better_msg, Viewtype::Text)
} else {
(&part.msg, part.typ)
@@ -1277,6 +1282,10 @@ RETURNING id
debug_assert!(!row_id.is_special());
created_db_entries.push(row_id);
if group_changes_msg.is_none() || (group_changes_msgs.is_empty() && better_msg.is_none()) {
parts.next();
}
}
// check all parts whether they contain a new logging webxdc
@@ -1596,7 +1605,7 @@ async fn create_or_lookup_group(
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
mime_parser.replace_msg_by_error(&s);
}
ProtectionStatus::Protected
} else {
@@ -1657,6 +1666,7 @@ async fn create_or_lookup_group(
members.push(from_id);
}
members.extend(to_ids);
members.sort_unstable();
members.dedup();
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
@@ -1695,6 +1705,7 @@ async fn create_or_lookup_group(
/// Apply group member list, name, avatar and protection status changes from the MIME message.
///
/// Optionally returns better message to replace the original system message.
/// is_partial_download: whether the message is not fully downloaded.
async fn apply_group_changes(
context: &Context,
mime_parser: &mut MimeMessage,
@@ -1702,15 +1713,16 @@ async fn apply_group_changes(
chat_id: ChatId,
from_id: ContactId,
to_ids: &[ContactId],
) -> Result<Option<String>> {
is_partial_download: bool,
) -> Result<Vec<String>> {
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Group {
return Ok(None);
return Ok(Vec::new());
}
let mut send_event_chat_modified = false;
let (mut removed_id, mut added_id) = (None, None);
let mut better_msg = None;
let mut group_changes_msgs = Vec::new();
// True if a Delta Chat client has explicitly added our current primary address.
let self_added =
@@ -1720,12 +1732,14 @@ async fn apply_group_changes(
false
};
let mut chat_contacts = HashSet::from_iter(chat::get_chat_contacts(context, chat_id).await?);
let mut chat_contacts =
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
let is_from_in_chat =
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
// Reject group membership changes from non-members and old changes.
let allow_member_list_changes = is_from_in_chat
let allow_member_list_changes = !is_partial_download
&& is_from_in_chat
&& chat_id
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
.await?;
@@ -1739,7 +1753,9 @@ async fn apply_group_changes(
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
// If we don't know the referenced message, we missed some messages.
// Maybe they added/removed members, so we need to recreate our member list.
Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(),
Some(reply_to) => rfc724_mid_exists_and(context, reply_to, "download_state=0")
.await?
.is_none(),
None => false,
}
} && {
@@ -1758,7 +1774,7 @@ async fn apply_group_changes(
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
mime_parser.replace_msg_by_error(&s);
}
if !chat.is_protected() {
@@ -1771,11 +1787,11 @@ async fn apply_group_changes(
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
better_msg = if removed_id == Some(from_id) {
Some(stock_str::msg_group_left_local(context, from_id).await)
group_changes_msgs.push(if removed_id == Some(from_id) {
stock_str::msg_group_left_local(context, from_id).await
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
stock_str::msg_del_member_local(context, removed_addr, from_id).await
});
if removed_id.is_some() {
if !allow_member_list_changes {
@@ -1788,7 +1804,8 @@ async fn apply_group_changes(
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
group_changes_msgs
.push(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if allow_member_list_changes {
if !recreate_member_list {
@@ -1829,21 +1846,20 @@ async fn apply_group_changes(
send_event_chat_modified = true;
}
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
group_changes_msgs
.push(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
}
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg = match avatar_action {
AvatarAction::Delete => {
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
}
group_changes_msgs.push(match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => {
Some(stock_str::msg_grp_img_changed(context, from_id).await)
stock_str::msg_grp_img_changed(context, from_id).await
}
};
});
}
}
}
@@ -1856,33 +1872,43 @@ async fn apply_group_changes(
}
if !recreate_member_list {
let diff: HashSet<ContactId> =
chat_contacts.difference(&new_members).copied().collect();
// Only delete old contacts if the sender is not a classical MUA user:
// Classical MUA users usually don't intend to remove users from an email
// thread, so if they removed a recipient then it was probably by accident.
if mime_parser.has_chat_version() {
// This is what provides group membership consistency: we remove group members
// locally if we see a discrepancy with the "To" list in the received message as it
// is better for privacy than adding absent members locally. But it shouldn't be a
// big problem if somebody missed a member addition, because they will likely
// recreate the member list from the next received message. The problem occurs only
// if that "somebody" managed to reply earlier. Really, it's a problem for big
// groups with high message rate, but let it be for now.
if !diff.is_empty() {
warn!(context, "Implicit removal of {diff:?} from chat {chat_id}.");
}
new_members = chat_contacts.difference(&diff).copied().collect();
} else {
new_members.extend(diff);
// Don't delete any members locally, but instead add absent ones to provide group
// membership consistency for all members:
// - Classical MUA users usually don't intend to remove users from an email thread, so
// if they removed a recipient then it was probably by accident.
// - DC users could miss new member additions and then better to handle this in the same
// way as for classical MUA messages. Moreover, if we remove a member implicitly, they
// will never know that and continue to think they're still here.
// But it shouldn't be a big problem if somebody missed a member removal, because they
// will likely recreate the member list from the next received message. The problem
// occurs only if that "somebody" managed to reply earlier. Really, it's a problem for
// big groups with high message rate, but let it be for now.
let mut diff: HashSet<ContactId> =
new_members.difference(&chat_contacts).copied().collect();
new_members = chat_contacts.clone();
new_members.extend(diff.clone());
if let Some(added_id) = added_id {
diff.remove(&added_id);
}
if !diff.is_empty() {
warn!(context, "Implicit addition of {diff:?} to chat {chat_id}.");
}
group_changes_msgs.reserve(diff.len());
for contact_id in diff {
let contact = Contact::get_by_id(context, contact_id).await?;
group_changes_msgs.push(
stock_str::msg_add_member_local(
context,
contact.get_addr(),
ContactId::UNDEFINED,
)
.await,
);
}
}
if let Some(removed_id) = removed_id {
new_members.remove(&removed_id);
}
if let Some(added_id) = added_id {
new_members.insert(added_id);
}
if recreate_member_list {
info!(
context,
@@ -1945,7 +1971,7 @@ async fn apply_group_changes(
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat_id));
}
Ok(better_msg)
Ok(group_changes_msgs)
}
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
@@ -2311,7 +2337,7 @@ async fn has_verified_encryption(
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
sql::repeat_vars(to_ids.len())
),
rusqlite::params_from_iter(to_ids),
rusqlite::params_from_iter(&to_ids),
|row| {
let to_addr: String = row.get(0)?;
let is_verified: i32 = row.get(1).unwrap_or(0);
@@ -2326,48 +2352,31 @@ async fn has_verified_encryption(
let contact = Contact::get_by_id(context, from_id).await?;
for (to_addr, mut is_verified) in rows {
info!(
context,
"has_verified_encryption: {:?} self={:?}.",
to_addr,
context.is_self_addr(&to_addr).await
);
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
for (to_addr, is_verified) in rows {
// mark gossiped keys (if any) as verified
if mimeparser.gossiped_addr.contains(&to_addr.to_lowercase()) {
if let Some(mut peerstate) = peerstate {
// if we're here, we know the gossip key is verified:
// - use the gossip-key as verified-key if there is no verified-key
// - OR if the verified-key does not match public-key or gossip-key
// (otherwise a verified key can _only_ be updated through QR scan which might be annoying,
// see <https://github.com/nextleap-project/countermitm/issues/46> for a discussion about this point)
if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
// If we're here, we know the gossip key is verified.
// Use the gossip-key as verified-key if there is no verified-key
// or a member is reintroduced to a verified group.
//
// See <https://github.com/nextleap-project/countermitm/issues/46>
// and <https://github.com/deltachat/deltachat-core-rust/issues/4541> for discussion.
let verifier_addr = contact.get_addr().to_owned();
if !is_verified
|| peerstate.verified_key_fingerprint != peerstate.public_key_fingerprint
&& peerstate.verified_key_fingerprint != peerstate.gossip_key_fingerprint
|| mimeparser
.get_header(HeaderDef::ChatGroupMemberAdded)
.filter(|s| s.as_str() == to_addr)
.is_some()
{
info!(context, "{} has verified {}.", contact.get_addr(), to_addr);
let fp = peerstate.gossip_key_fingerprint.clone();
if let Some(fp) = fp {
peerstate.set_verified(
PeerstateKeyType::GossipKey,
fp,
PeerstateVerifiedStatus::BidirectVerified,
contact.get_addr().to_owned(),
)?;
info!(context, "{verifier_addr} has verified {to_addr}.");
if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
peerstate.set_verified(PeerstateKeyType::GossipKey, fp, verifier_addr)?;
peerstate.save_to_db(&context.sql).await?;
is_verified = true;
}
}
}
}
if !is_verified {
return Ok(NotVerified(format!(
"{} is not a member of this protected chat",
to_addr
)));
}
}
Ok(Verified)
}

View File

@@ -3117,6 +3117,46 @@ async fn test_thunderbird_autocrypt() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> {
let t = TestContext::new_bob().await;
let raw =
include_bytes!("../../test-data/message/thunderbird_encrypted_signed_with_pubkey.eml");
receive_imf(&t, raw, false).await?;
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
.await?
.unwrap();
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
receive_imf(
&t,
b"From: alice@example.org\n\
To: bob@example.net\n\
Subject: foo\n\
Message-ID: <message@example.org>\n\
Date: Thu, 2 Nov 2023 02:20:28 -0300\n\
\n\
unencrypted\n",
false,
)
.await?;
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
.await?
.unwrap();
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
receive_imf(&t, raw, false).await?;
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
.await?
.unwrap();
assert!(peerstate.public_key.is_some());
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> {
let t = TestContext::new_bob().await;
@@ -3371,14 +3411,24 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
alice.recv_msg(&bob.pop_sent_msg().await).await;
// Bob didn't receive the addition of Fiona, so Alice must remove Fiona from the members list
// back to make their group members view consistent.
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
// Bob didn't receive the addition of Fiona, but Alice mustn't remove Fiona from the members
// list back. Instead, Bob must add Fiona from the next Alice's message to make their group
// members view consistent.
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
// Just a dumb check for remove_contact_from_chat(). Let's have it in this only place.
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
send_text_msg(
&alice,
alice_chat_id,
"Finally add Fiona please".to_string(),
)
.await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
Ok(())
}
@@ -3462,8 +3512,11 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
bob.recv_msg(&alice.pop_sent_msg().await).await;
// Alice didn't receive Bobs leave message, but bob shouldn't readded himself just because of that.
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
// Alice didn't receive Bob's leave message, so Bob must readd themselves otherwise other
// members would think Bob is still here while they aren't, and then retry to leave if they
// think that Alice didn't re-add them on purpose (which is possible if Alice uses a classical
// MUA).
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
Ok(())
}
@@ -3810,3 +3863,113 @@ async fn test_create_group_with_big_msg() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_group_consistency() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
let bob = tcm.bob().await;
bob.recv_msg(&add).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 2);
// Get initial timestamp.
let timestamp = bob_chat_id
.get_param(&bob)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap();
// Bob receives partial message.
let msg_id = receive_imf_inner(
&bob,
"first@example.org",
b"From: Alice <alice@example.org>\n\
To: <bob@example.net>, <charlie@example.com>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain
Chat-Group-Member-Added: charlie@example.com",
false,
Some(100000),
false,
)
.await?
.context("no received message")?;
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
let timestamp2 = bob_chat_id
.get_param(&bob)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap();
// Partial download does not change the member list.
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(timestamp, timestamp2);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
// Alice sends normal message to bob, adding fiona.
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "fiona", "fiona@example.net").await?,
)
.await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let timestamp3 = bob_chat_id
.get_param(&bob)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap();
// Receiving a message after a partial download recreates the member list because we treat
// such messages as if we have not seen them.
assert_ne!(timestamp, timestamp3);
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 3);
// Bob fully reives the partial message.
let msg_id = receive_imf_inner(
&bob,
"first@example.org",
b"From: Alice <alice@example.org>\n\
To: Bob <bob@example.net>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain
Chat-Group-Member-Added: charlie@example.com",
false,
None,
false,
)
.await?
.context("no received message")?;
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
let timestamp4 = bob_chat_id
.get_param(&bob)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap();
// After full download, the old message should not change group state.
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(timestamp3, timestamp4);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
Ok(())
}

View File

@@ -620,7 +620,7 @@ async fn fetch_idle(
.idle(
ctx,
connection.idle_interrupt_receiver.clone(),
Some(watch_folder),
&watch_folder,
)
.await
.context("idle")

View File

@@ -8,8 +8,8 @@ use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype};
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::ensure_secret_key_exists;
use crate::events::EventType;
@@ -18,7 +18,7 @@ use crate::key::{load_self_public_key, DcKey, Fingerprint};
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::peerstate::{Peerstate, PeerstateKeyType};
use crate::qr::check_qr;
use crate::stock_str;
use crate::token;
@@ -36,17 +36,15 @@ use crate::token::Namespace;
/// Set of characters to percent-encode in email addresses and names.
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
macro_rules! inviter_progress {
($context:tt, $contact_id:expr, $progress:expr) => {
assert!(
$progress >= 0 && $progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.emit_event($crate::events::EventType::SecurejoinInviterProgress {
contact_id: $contact_id,
progress: $progress,
});
};
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
debug_assert!(
progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
context.emit_event(EventType::SecurejoinInviterProgress {
contact_id,
progress,
});
}
/// Generates a Secure Join QR code.
@@ -318,7 +316,7 @@ pub(crate) async fn handle_securejoin_handshake(
}
info!(context, "Secure-join requested.",);
inviter_progress!(context, contact_id, 300);
inviter_progress(context, contact_id, 300);
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
@@ -415,10 +413,9 @@ pub(crate) async fn handle_securejoin_handshake(
.await?
.get_addr()
.to_owned();
if mark_peer_as_verified(context, fingerprint.clone(), contact_addr)
.await
.is_err()
{
let fingerprint_found =
mark_peer_as_verified(context, fingerprint.clone(), contact_addr).await?;
if !fingerprint_found {
could_not_establish_secure_connection(
context,
contact_id,
@@ -431,7 +428,7 @@ pub(crate) async fn handle_securejoin_handshake(
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
info!(context, "Auth verified.",);
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
inviter_progress(context, contact_id, 600);
if join_vg {
// the vg-member-added message is special:
// this is a normal Chat-Group-Member-Added message
@@ -446,17 +443,13 @@ pub(crate) async fn handle_securejoin_handshake(
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
Some((group_chat_id, _, _)) => {
secure_connection_established(context, contact_id, group_chat_id).await?;
if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await
{
error!(context, "failed to add contact: {}", err);
}
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await?;
}
None => bail!("Chat {} not found", &field_grpid),
}
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
inviter_progress(context, contact_id, 800);
inviter_progress(context, contact_id, 1000);
} else {
// Alice -> Bob
secure_connection_established(
@@ -474,53 +467,52 @@ pub(crate) async fn handle_securejoin_handshake(
.await
.context("failed sending vc-contact-confirm message")?;
inviter_progress!(context, contact_id, 1000);
inviter_progress(context, contact_id, 1000);
}
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
}
"vg-member-added" | "vc-contact-confirm" => {
/*=======================================================
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
=======================================================*/
/*=======================================================
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
=======================================================*/
"vc-contact-confirm" => match BobState::from_db(&context.sql).await? {
Some(bobstate) => bob::handle_contact_confirm(context, bobstate, mime_message).await,
None => Ok(HandshakeMessage::Ignore),
},
"vg-member-added" => {
let Some(member_added) = mime_message
.get_header(HeaderDef::ChatGroupMemberAdded)
.map(|s| s.as_str())
else {
warn!(
context,
"vg-member-added without Chat-Group-Member-Added header"
);
return Ok(HandshakeMessage::Propagate);
};
if !context.is_self_addr(member_added).await? {
info!(
context,
"Member {member_added} added by unrelated SecureJoin process"
);
return Ok(HandshakeMessage::Propagate);
}
match BobState::from_db(&context.sql).await? {
Some(bobstate) => {
bob::handle_contact_confirm(context, bobstate, mime_message).await
}
None => match join_vg {
true => Ok(HandshakeMessage::Propagate),
false => Ok(HandshakeMessage::Ignore),
},
None => Ok(HandshakeMessage::Propagate),
}
}
"vg-member-added-received" | "vc-contact-confirm-received" => {
/*==========================================================
==== Alice - the inviter side ====
==== Step 8 in "Out-of-band verified groups" protocol ====
==========================================================*/
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
if contact.is_verified(context).await? == VerifiedStatus::Unverified {
warn!(context, "{} invalid.", step);
return Ok(HandshakeMessage::Ignore);
}
if join_vg {
let field_grpid = mime_message
.get_header(HeaderDef::SecureJoinGroup)
.map(|s| s.as_str())
.unwrap_or_else(|| "");
if let Err(err) = chat::get_chat_id_by_grpid(context, field_grpid).await {
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
return Err(
err.context(format!("Chat for group {} not found", &field_grpid))
);
}
}
Ok(HandshakeMessage::Ignore) // "Done" deletes the message and breaks multi-device
} else {
warn!(context, "{} invalid.", step);
Ok(HandshakeMessage::Ignore)
}
Ok(HandshakeMessage::Done) // "Done" deletes the message
}
_ => {
warn!(context, "invalid step: {}", step);
@@ -614,21 +606,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore);
}
};
if let Err(err) = peerstate.set_verified(
PeerstateKeyType::GossipKey,
fingerprint,
PeerstateVerifiedStatus::BidirectVerified,
addr,
) {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
&format!("Could not mark peer as verified at step {step}: {err}"),
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
peerstate.set_verified(PeerstateKeyType::GossipKey, fingerprint, addr)?;
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
} else if let Some(fingerprint) =
@@ -637,7 +615,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
// FIXME: Old versions of DC send this header instead of gossips. Remove this
// eventually.
let fingerprint = fingerprint.parse()?;
if mark_peer_as_verified(
let fingerprint_found = mark_peer_as_verified(
context,
fingerprint,
Contact::get_by_id(context, contact_id)
@@ -645,9 +623,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
.get_addr()
.to_owned(),
)
.await
.is_err()
{
.await?;
if !fingerprint_found {
could_not_establish_secure_connection(
context,
contact_id,
@@ -672,10 +649,10 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore);
}
if step.as_str() == "vg-member-added" {
inviter_progress!(context, contact_id, 800);
inviter_progress(context, contact_id, 800);
}
if step.as_str() == "vg-member-added" || step.as_str() == "vc-contact-confirm" {
inviter_progress!(context, contact_id, 1000);
inviter_progress(context, contact_id, 1000);
}
if step.as_str() == "vg-request-with-auth" || step.as_str() == "vc-request-with-auth" {
// This actually reflects what happens on the first device (which does the secure
@@ -705,17 +682,17 @@ async fn secure_connection_established(
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
{
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ == Chattype::Single {
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact_id),
)
.await?;
}
let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
.await?
.id;
private_chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact_id),
)
.await?;
}
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
@@ -737,27 +714,21 @@ async fn could_not_establish_secure_connection(
Ok(())
}
/// Tries to mark peer with provided key fingerprint as verified.
///
/// Returns true if such key was found, false otherwise.
async fn mark_peer_as_verified(
context: &Context,
fingerprint: Fingerprint,
verifier: String,
) -> Result<()> {
if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? {
if let Err(err) = peerstate.set_verified(
PeerstateKeyType::PublicKey,
fingerprint,
PeerstateVerifiedStatus::BidirectVerified,
verifier,
) {
error!(context, "Could not mark peer as verified: {}", err);
return Err(err);
}
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
Ok(())
} else {
bail!("no peerstate in db for fingerprint {}", fingerprint.hex());
}
) -> Result<bool> {
let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? else {
return Ok(false);
};
peerstate.set_verified(PeerstateKeyType::PublicKey, fingerprint, verifier)?;
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
Ok(true)
}
/* ******************************************************************************
@@ -794,6 +765,7 @@ mod tests {
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactAddress;
use crate::contact::VerifiedStatus;
use crate::peerstate::Peerstate;

View File

@@ -15,6 +15,7 @@ use crate::contact::Contact;
use crate::context::Context;
use crate::events::EventType;
use crate::mimeparser::MimeMessage;
use crate::sync::Sync::*;
use crate::tools::time;
use crate::{chat, stock_str};
@@ -110,7 +111,7 @@ pub(super) async fn handle_auth_required(
/// Handles `vc-contact-confirm` and `vg-member-added` handshake messages.
///
/// # Bob - the joiner's side
/// ## Step 4 in the "Setup Contact protocol"
/// ## Step 7 in the "Setup Contact protocol"
pub(super) async fn handle_contact_confirm(
context: &Context,
mut bobstate: BobState,
@@ -131,6 +132,7 @@ pub(super) async fn handle_contact_confirm(
// verify both contacts (this could be a bug/security issue, see
// e.g. https://github.com/deltachat/deltachat-core-rust/issues/1177).
bobstate.notify_peer_verified(context).await?;
bobstate.emit_progress(context, JoinerProgress::Succeeded);
Ok(retval)
}
Some(_) => {
@@ -179,7 +181,7 @@ impl BobState {
} => {
let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
Some((chat_id, _protected, _blocked)) => {
chat_id.unblock(context).await?;
chat_id.unblock_ex(context, Nosync).await?;
chat_id
}
None => {
@@ -227,9 +229,8 @@ impl BobState {
if context
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
&& chat_id == self.alice_chat()
{
chat_id
self.alice_chat()
.set_protection(
context,
ProtectionStatus::Protected,
@@ -255,8 +256,8 @@ enum JoinerProgress {
///
/// Typically shows as "alice@addr verified, introducing myself."
RequestWithAuthSent,
// /// Completed securejoin.
// Succeeded,
/// Completed securejoin.
Succeeded,
}
impl From<JoinerProgress> for usize {
@@ -264,7 +265,7 @@ impl From<JoinerProgress> for usize {
match progress {
JoinerProgress::Error => 0,
JoinerProgress::RequestWithAuthSent => 400,
// JoinerProgress::Succeeded => 1000,
JoinerProgress::Succeeded => 1000,
}
}
}

View File

@@ -7,7 +7,7 @@
//! The [`BobState`] is only directly used to initially create it when starting the
//! protocol.
use anyhow::{Error, Result};
use anyhow::Result;
use rusqlite::Connection;
use super::qrinvite::QrInvite;
@@ -335,36 +335,6 @@ impl BobState {
context,
"Bob Step 7 - handling vc-contact-confirm/vg-member-added message"
);
let vg_expect_encrypted = match self.invite {
QrInvite::Contact { .. } => {
// setup-contact is always encrypted
true
}
QrInvite::Group { ref grpid, .. } => {
// This is buggy, result will always be
// false since the group is created by receive_imf for
// the very handshake message we're handling now. But
// only after we have returned. It does not impact
// the security invariants of secure-join however.
chat::get_chat_id_by_grpid(context, grpid)
.await?
.map_or(false, |(_chat_id, is_protected, _blocked)| is_protected)
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
// So only expect encryption if this is a verified group
}
};
if vg_expect_encrypted
&& !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint()))
{
self.update_next(&context.sql, SecureJoinStep::Terminated)
.await?;
return Ok(Some(BobHandshakeStage::Terminated(
"Contact confirm message not encrypted",
)));
}
mark_peer_as_verified(
context,
self.invite.fingerprint().clone(),
@@ -375,17 +345,6 @@ impl BobState {
.await?;
context.emit_event(EventType::ContactsChanged(None));
if let QrInvite::Group { .. } = self.invite {
let member_added = mime_message
.get_header(HeaderDef::ChatGroupMemberAdded)
.map(|s| s.as_str())
.ok_or_else(|| Error::msg("Missing Chat-Group-Member-Added header"))?;
if !context.is_self_addr(member_added).await? {
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
return Ok(None);
}
}
self.send_handshake_message(context, BobHandshakeMsg::ContactConfirmReceived)
.await
.map_err(|_| {

View File

@@ -413,6 +413,12 @@ pub enum StockMessage {
#[strum(props(fallback = "%1$s sent a message from another device."))]
ChatProtectionDisabled = 171,
#[strum(props(fallback = "Others will only see this group after you sent a first message."))]
NewGroupSendFirstMessage = 172,
#[strum(props(fallback = "Member %1$s added."))]
MsgAddMember = 173,
}
impl StockMessage {
@@ -600,6 +606,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
}
/// Stock string: `I added member %1$s.`.
/// This one is for sending in group chats.
///
/// The `added_member_addr` parameter should be an email address and is looked up in the
/// contacts to combine with the authorized display name.
@@ -634,7 +641,11 @@ pub(crate) async fn msg_add_member_local(
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
if by_contact == ContactId::SELF {
if by_contact == ContactId::UNDEFINED {
translated(context, StockMessage::MsgAddMember)
.await
.replace1(whom)
} else if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouAddMember)
.await
.replace1(whom)
@@ -1284,6 +1295,11 @@ pub(crate) async fn aeap_explanation_and_link(
.replace2(new_addr)
}
/// Stock string: `Others will only see this group after you sent a first message.`.
pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
translated(context, StockMessage::NewGroupSendFirstMessage).await
}
/// Text to put in the [`Qr::Backup`] rendered SVG image.
///
/// The default is "Scan to set up second device for <account name (account addr)>". The

View File

@@ -5,7 +5,7 @@ use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
use crate::chat::{Chat, ChatId};
use crate::chat::{self, Chat, ChatVisibility};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::ContactId;
@@ -13,10 +13,26 @@ use crate::context::Context;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
use crate::token::Namespace;
use crate::tools::time;
use crate::{chat, stock_str, token};
use crate::{stock_str, token};
/// Whether to send device sync messages. Aimed for usage in the internal API.
#[derive(Debug)]
pub(crate) enum Sync {
Nosync,
Sync,
}
impl From<Sync> for bool {
fn from(sync: Sync) -> bool {
match sync {
Sync::Nosync => false,
Sync::Sync => true,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct QrTokenData {
@@ -25,10 +41,28 @@ pub(crate) struct QrTokenData {
pub(crate) grpid: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum ChatId {
ContactAddr(String),
Grpid(String),
// NOTE: Ad-hoc groups lack an identifier that can be used across devices so
// block/mute/etc. actions on them are not synchronized to other devices.
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum ChatAction {
Block,
Unblock,
Accept,
SetVisibility(ChatVisibility),
SetMuted(chat::MuteDuration),
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum SyncData {
AddQrToken(QrTokenData),
DeleteQrToken(QrTokenData),
AlterChat { id: ChatId, action: ChatAction },
}
#[derive(Debug, Serialize, Deserialize)]
@@ -44,6 +78,9 @@ pub(crate) struct SyncItems {
impl Context {
/// Adds an item to the list of items that should be synchronized to other devices.
///
/// NB: Private and `pub(crate)` functions shouldn't call this unless `Sync::Sync` is explicitly
/// passed to them. This way it's always clear whether the code performs synchronisation.
pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
self.add_sync_item_with_timestamp(data, time()).await
}
@@ -67,7 +104,7 @@ impl Context {
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
/// If device synchronization is disabled,
/// no tokens exist or the chat is unpromoted, the function does nothing.
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<chat::ChatId>) -> Result<()> {
if !self.get_config_bool(Config::SyncMsgs).await? {
return Ok(());
}
@@ -118,7 +155,7 @@ impl Context {
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
if let Some((json, ids)) = self.build_sync_json().await? {
let chat_id =
ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
chat::ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
.await?;
let mut msg = Message {
chat_id,
@@ -204,7 +241,7 @@ impl Context {
Ok(sync_items)
}
/// Execute sync items.
/// Executes sync items sent by other device.
///
/// CAVE: When changing the code to handle other sync items,
/// take care that does not result in calls to `add_sync_item()`
@@ -243,6 +280,7 @@ impl Context {
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
token::delete(self, Namespace::Auth, &token.auth).await?;
}
AlterChat { id, action } => self.sync_alter_chat(id, action).await?,
}
}
Ok(())
@@ -251,11 +289,15 @@ impl Context {
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use anyhow::bail;
use strum::IntoEnumIterator;
use super::*;
use crate::chat::Chat;
use crate::chat::{Chat, ProtectionStatus};
use crate::chatlist::Chatlist;
use crate::contact::{Contact, Origin};
use crate::test_utils::TestContext;
use crate::token::Namespace;
@@ -277,6 +319,20 @@ mod tests {
assert!(t.build_sync_json().await?.is_none());
// Having one test on `SyncData::AlterChat` is sufficient here as `ChatAction::SetMuted`
// introduces enums inside items and `SystemTime`. Let's avoid in-depth testing of the
// serialiser here which is an external crate.
t.add_sync_item_with_timestamp(
SyncData::AlterChat {
id: ChatId::ContactAddr("bob@example.net".to_string()),
action: ChatAction::SetMuted(chat::MuteDuration::Until(
SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
)),
},
1631781315,
)
.await?;
t.add_sync_item_with_timestamp(
SyncData::AddQrToken(QrTokenData {
invitenumber: "testinvite".to_string(),
@@ -300,6 +356,7 @@ mod tests {
assert_eq!(
serialized,
r#"{"items":[
{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}},
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
]}"#
@@ -310,7 +367,7 @@ mod tests {
assert!(t.build_sync_json().await?.is_none());
let sync_items = t.parse_sync_items(serialized)?;
assert_eq!(sync_items.items.len(), 2);
assert_eq!(sync_items.items.len(), 3);
Ok(())
}
@@ -368,6 +425,27 @@ mod tests {
)
.is_err()); // missing field
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#.to_string(),
)
.is_err()); // Unknown enum value
// Test enums inside items and SystemTime
let sync_items = t.parse_sync_items(
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}}]}"#.to_string(),
)?;
assert_eq!(sync_items.items.len(), 1);
let AlterChat { id, action } = &sync_items.items.get(0).unwrap().data else {
bail!("bad item");
};
assert_eq!(*id, ChatId::ContactAddr("bob@example.net".to_string()));
assert_eq!(
*action,
ChatAction::SetMuted(chat::MuteDuration::Until(
SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
))
);
// empty item list is okay
assert_eq!(
t.parse_sync_items(r#"{"items":[]}"#.to_string())?
@@ -423,6 +501,7 @@ mod tests {
let sync_items = t
.parse_sync_items(
r#"{"items":[
{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Block"}}},
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistent, shall continue"}}},
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
@@ -435,6 +514,11 @@ mod tests {
?;
t.execute_sync_items(&sync_items).await?;
assert!(
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
.await?
.is_none()
);
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await);
assert!(token::exists(&t, Namespace::Auth, "yip-auth").await);
assert!(!token::exists(&t, Namespace::Auth, "non-existent").await);
@@ -462,7 +546,7 @@ mod tests {
// check that the used self-talk is not visible to the user
// but that creation will still work (in this case, the chat is empty)
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
let chat_id = chat::ChatId::create_for_contact(&alice, ContactId::SELF).await?;
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert!(chat.is_self_talk());
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
@@ -485,4 +569,124 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alter_chat() -> Result<()> {
let alices = [
TestContext::new_alice().await,
TestContext::new_alice().await,
];
for a in &alices {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(&alices[0]).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
alices[1].recv_msg(&sent_msg).await;
async fn sync(alices: &[TestContext]) -> Result<()> {
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
alices.get(1).unwrap().recv_msg(&sync_msg).await;
Ok(())
}
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
a0b_chat_id.block(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
a0b_chat_id.unblock(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
// Unblocking a 1:1 chat doesn't unblock the contact currently.
let a0b_contact_id = alices[0].add_or_lookup_contact(&bob).await.id;
Contact::unblock(&alices[0], a0b_contact_id).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::block(&alices[0], a0b_contact_id).await?;
sync(&alices).await?;
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::unblock(&alices[0], a0b_contact_id).await?;
sync(&alices).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
// Test accepting and blocking groups. This way we test:
// - Group chats synchronisation.
// - That blocking a group deletes it on other devices.
let fiona = TestContext::new_fiona().await;
let fiona_grp_chat_id = fiona
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
.await;
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
a0_grp_chat_id.accept(&alices[0]).await?;
sync(&alices).await?;
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
a0_grp_chat_id.block(&alices[0]).await?;
sync(&alices).await?;
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
.await
.is_err());
assert!(
!alices[1]
.sql
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
.await?
);
// Test syncing of chat visibility on a self-chat. This way we test:
// - Self-chat synchronisation.
// - That sync messages don't unarchive the self-chat.
let a0self_chat_id = alices[0].get_self_chat().await.id;
assert_eq!(
alices[1].get_self_chat().await.get_visibility(),
ChatVisibility::Normal
);
let mut visibilities =
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
visibilities.next();
for v in visibilities {
a0self_chat_id.set_visibility(&alices[0], v).await?;
sync(&alices).await?;
for a in &alices {
assert_eq!(a.get_self_chat().await.get_visibility(), v);
}
}
use chat::MuteDuration;
assert_eq!(
alices[1].get_chat(&bob).await.mute_duration,
MuteDuration::NotMuted
);
let mute_durations = [
MuteDuration::Forever,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)),
MuteDuration::NotMuted,
];
for m in mute_durations {
chat::set_muted(&alices[0], a0b_chat_id, m).await?;
sync(&alices).await?;
let m = match m {
MuteDuration::Until(time) => MuteDuration::Until(
SystemTime::UNIX_EPOCH
+ Duration::from_secs(
time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
),
),
_ => m,
};
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
}
Ok(())
}
}

View File

@@ -374,6 +374,11 @@ impl TestContext {
}
});
ctx.set_config(Config::SkipStartMessages, Some("1"))
.await
.unwrap();
ctx.set_config(Config::SyncMsgs, Some("0")).await.unwrap();
Self {
ctx,
dir,
@@ -509,12 +514,7 @@ impl TestContext {
.await
.expect("receive_imf() seems not to have added a new message to the db");
assert_eq!(
received.msg_ids.len(),
1,
"recv_msg() can currently only receive messages with exactly one part"
);
let msg = Message::load_from_db(self, received.msg_ids[0])
let msg = Message::load_from_db(self, *received.msg_ids.last().unwrap())
.await
.unwrap();

View File

@@ -0,0 +1,7 @@
Group#Chat#10: Group chat [4 member(s)]
--------------------------------------------------------------------------------
Msg#10: (Contact#Contact#10): Hi! I created a group. [FRESH]
Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] o
Msg#12: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
Msg#13: (Contact#Contact#10): Member Me (bob@example.net) added. [FRESH][INFO]
--------------------------------------------------------------------------------

View File

@@ -0,0 +1,80 @@
From - Thu, 02 Nov 2023 05:25:44 GMT
X-Mozilla-Status: 0801
X-Mozilla-Status2: 00000000
Message-ID: <619b4cc5-1b76-2bfc-02ee-631b774bcb4a@example.org>
Date: Thu, 2 Nov 2023 02:25:44 -0300
MIME-Version: 1.0
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
Thunderbird/102.13.0
Content-Language: en-US
To: bob@example.net
From: Alice <alice@example.org>
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
attachmentreminder=0; deliveryformat=0
X-Identity-Key: id3
Fcc: imap://alice%40example.org@in.example.org/Sent
Subject: ...
Content-Type: multipart/encrypted;
protocol="application/pgp-encrypted";
boundary="------------ODMSY6iRxlSk4uG4zIxjzLbw"
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
--------------ODMSY6iRxlSk4uG4zIxjzLbw
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
Version: 1
--------------ODMSY6iRxlSk4uG4zIxjzLbw
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
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
d+EkgKXHfQVfohZhSg==
=u1yY
-----END PGP MESSAGE-----
--------------ODMSY6iRxlSk4uG4zIxjzLbw--

View File

@@ -0,0 +1,165 @@
From - Thu, 02 Nov 2023 05:20:27 GMT
X-Mozilla-Status: 0801
X-Mozilla-Status2: 00000000
Message-ID: <956fad6d-206e-67af-2443-3ea5819418ff@example.org>
Date: Thu, 2 Nov 2023 02:20:27 -0300
MIME-Version: 1.0
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
Thunderbird/102.13.0
Content-Language: en-US
To: bob@example.net
From: Alice <alice@example.org>
Autocrypt: addr=alice@example.org; keydata=
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
/qMLjKwBpKEd/w==
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
attachmentreminder=0; deliveryformat=0
X-Identity-Key: id3
Fcc: imap://alice%40example.org@in.example.org/Sent
Subject: ...
Content-Type: multipart/encrypted;
protocol="application/pgp-encrypted";
boundary="------------vm4cgC4UBxQhCLFg6Vv0nrTd"
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
--------------vm4cgC4UBxQhCLFg6Vv0nrTd
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
Version: 1
--------------vm4cgC4UBxQhCLFg6Vv0nrTd
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
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
=UKyr
-----END PGP MESSAGE-----
--------------vm4cgC4UBxQhCLFg6Vv0nrTd--

View File

@@ -38,11 +38,11 @@ While this describes an already working "proof of concept" usage there are alrea
webxdc empowers FOSS developments in unprecedented ways:
- well-known paradim: use all the existing JS/html5 libraries and designs of your choice
- well-known paradigm: use all the existing JS/html5 libraries and designs of your choice
- quick onboarding: only a handful API methods to learn
- serverless (but really): no worrying about hosting a server or configuring DNS, AWS etc
- permissionless: no worrying about registering at app stores for distribution
- unbuerocratic: no worrying about login/password/expiry procedures or leaks
- unbureaucratic: no worrying about login/password/expiry procedures or leaks
- secure: no worrying about cryptographic algos or e2e-encryption complexity
On the flip side, you need to learn how to do state updates between instances of your webxdc apps. This is a classic P2P problem and there are simple (send full state update) and advanced ways (use CRDTs or similar) to arrange decentralized state. In any case, there is no DHT let alone blockchain needed and thus no Crypto or coin needed, either.