Compare commits

..

70 Commits

Author SHA1 Message Date
holger krekel
eb61a6cf3b replace wrong link to countermitm with s3ecurejoin 2023-11-22 15:21:07 +01:00
link2xt
30f8522626 docs: generate deltachat-rpc-client documentation
To preview the docs, run:
```
scripts/build-python-docs.sh
firefox dist/html/index.html
```

I have removed the Makefile because modern Sphinx Makefile is just a
wrapper for `sphinx-build -M`:
3596590317/sphinx/templates/quickstart/Makefile.new_t
and sphinx-quickstart even has an option `--no-makefile`.
`make.bat` makes even less sense.
In `scripts/build-python-docs.sh` I use `sphinx-build` directly
without `make` wrapper.
2023-11-20 19:56:22 +00:00
link2xt
d3c221e061 chore: update dependencies 2023-11-20 19:52:40 +00:00
link2xt
8a421224f8 chore(release): prepare for 1.131.5 2023-11-20 19:04:57 +00:00
link2xt
7dfce71ac9 fix: lowercase addr when it is set
Prevent users from creating new accounts with uppercase letters
in the address.
2023-11-20 16:46:59 +00:00
link2xt
35ba97f76a fix: lowercase the address in member added/removed messages 2023-11-20 16:46:59 +00:00
link2xt
41921eaf3d fix: compare verifier addr to peerstate addr with addr_cmp 2023-11-20 16:46:59 +00:00
link2xt
03221ea86c refactor: lowercase the address in addr_normalize() 2023-11-20 16:46:59 +00:00
link2xt
b50761e4d1 api: turn ContactAddress into an owned type
This allows to normalize the address on creation more,
e.g. lowercase it.
2023-11-20 16:46:59 +00:00
link2xt
40dea771cc ci: test with Rust 1.74 2023-11-19 21:41:28 +00:00
link2xt
e011f8f42f chore: fix Rust 1.74 clippy warning 2023-11-19 21:40:19 +00:00
link2xt
09d4b4354a feat: lowercase addresses in Autocrypt and Autocrypt-Gossip
Email addresses should generally be compared case-insensitively,
but there may be errors in comparison code.
To reduce the chance of problems, encode addresses
in Autocrypt and Autocrypt-Gossip in lowercase
to avoid propagating uppercase characters over the network
to other accounts potentially running buggy code.
2023-11-19 06:22:25 +00:00
link2xt
ab151654fb chore: remove unused import from python tests 2023-11-18 11:32:34 +01:00
link2xt
ea9556b1b9 test: port test_aeap_flow_verified to JSON-RPC 2023-11-18 11:32:34 +01:00
link2xt
3dc6fd5c10 api(deltachat-rpc-client): add Message.get_sender_contact() 2023-11-18 11:32:34 +01:00
link2xt
f39acbc037 test: port test_qr_new_group_unblocked() to JSON-RPC 2023-11-18 11:32:34 +01:00
link2xt
005f7ff07e test: port test_qr_join_chat_with_pending_bobstate_issue4894 to JSON-RPC 2023-11-18 11:32:34 +01:00
link2xt
144ca7c171 test: port test_qr_join_chat to JSON-RPC
Disabled `verified_one_on_one_chats` is not tested
as it is not interesting, other checks are moved.
2023-11-18 11:32:34 +01:00
link2xt
7012b99d73 test: remove test_qr_setup_contact from online python tests
There is an identical `test_qr_setup_contact` in deltachat-rpc-client
test suite which also checks that contact profiles get verified.
2023-11-18 11:32:34 +01:00
Hocuri
72bacd56f7 Update securejoin links 2023-11-18 11:30:59 +01:00
iequidoo
cc75038ccc docs: Contributing guidelines for error handling 2023-11-18 00:34:47 -03:00
link2xt
f4810125e3 fix: recognize Chat-Group-Member-Added of self case-insensitively
If configured address is `Bob@example.net`,
but the message arrives adding `bob@example.net`,
Bob's device should still recognize it as addition of self
and fully recreate the group.
2023-11-18 02:42:28 +00:00
link2xt
acf1faf151 refactor(deltachat-rpc-client): add helper functions to wait for securejoin 2023-11-17 13:32:20 +00:00
link2xt
255fbe94f7 fix: do not use square brackets error for unknown sender
If the sender of the message in protected group chat
is not a member of the chat, mark the sender name with `~`
as we do it in non-protected chats and set the error
instead of replacing the whole message with
"Unknown sender for this chat. See 'info' for more details."

To send a message to a protected group this way
the sender needs to know the group ID
and sign the message with the current verified key.
Usually this is just a late message
delivered shortly after the user has left
the group or was removed from it.

Replacing the message with a single error text part
as done before this change makes it impossible
to access anything other than text, such as attached images.
2023-11-17 10:26:38 +00:00
link2xt
b5d1eba28e docs: add missing 1.131.4 link to the changelog 2023-11-17 10:13:18 +00:00
iequidoo
1509978738 fix: chat::rename_ex(): Sync improved chat name to other devices
Other devices should get the same chat name as the currently used device, i.e. the name a user sees
after renaming the chat. This fix is minor because `improve_single_line_input()` logic isn't going
to change often, but still, and also it simplifies the code.
2023-11-17 03:55:50 -03:00
iequidoo
607b9e55a9 fix: Chat::sync_contacts(): Fetch contact addresses in a single query
In order to protect from races with contacts removal and (just in case) dangling contact ids in
`chats_contacts` table.
2023-11-17 03:55:50 -03:00
holger krekel
7c4c980409 for testrun.org subdomains we can allow 60 messages per minute (or lift the limit completely but maybe good to protect against wild-running bots or so) 2023-11-17 06:50:19 +00:00
B. Petersen
b8ad3ec1b1 chore(release): prepare for 1.131.4 2023-11-16 21:04:19 +01:00
link2xt
87dd33f66e fix: always add "Member added" as system message 2023-11-16 01:54:45 +00:00
link2xt
7d8d13759a docs: document DC_DOWNLOAD_UNDECIPHERABLE
This was introduced in https://github.com/deltachat/deltachat-core-rust/pull/4685
for internal use when looking up a chat based on In-Reply-To,
but actually affects the UIs as they should not display Download button
when the message is downloaded but cannot be decrypted.
2023-11-15 23:07:10 +00:00
link2xt
2b4f2a9171 chore(release): prepare for 1.131.3 2023-11-15 20:08:20 +00:00
link2xt
8e869de350 fix(sync): skip sync when chat name is set to the current one 2023-11-15 19:18:13 +00:00
link2xt
b0ef082b2a fix(sync): ignore unknown sync items to provide forward compatibility 2023-11-15 19:17:38 +00:00
link2xt
bf8e74198d chore: update dependencies 2023-11-15 18:40:15 +00:00
link2xt
e77805471c fix: reset gossiped timestamp on securejoin
If verified key for a contact is changed via securejoin,
gossip the keys in every group with this contact next time
we send a message there to let others learn new verified key
and let the contact who has resetup their device learn keys of others
in groups.
2023-11-15 17:27:37 +00:00
link2xt
45a8004b33 fix: update async-imap to 0.9.4 which does not ignore EOF on FETCH 2023-11-15 15:59:29 +01:00
Simon Laux
990f4dce9b return connectivity html even when IO is stopped.
The returned error is unexpected and no UI I tested with stoped IO really handled it besides maybe displaying a toast.
(desktop and iOS don't handle it, deltatouch shows a toast)

This should not be shown to the user, it is only shown to the user if the UI has a bug, so that bug should be clearly visible.
2023-11-15 13:58:12 +01:00
link2xt
0f36197c54 fix: substitute variables in STATUS error logs 2023-11-15 10:57:05 +00:00
link2xt
890a2bcc15 chore(release): prepare for v1.131.2 2023-11-14 10:13:35 +00:00
link2xt
224355e83a fix: add "setup changed" message for verified key before the message
Merge the code paths for verified and autocrypt key.
If both are changed, only one will be added.

Existing code path adds a message to all chats with the contact
rather than to 1:1 chat. If we later decide that
only 1:1 chat or only verified chats should be notified,
we can add a separate `verified_fingerprint_changed` flag.
2023-11-14 10:00:25 +00:00
link2xt
c6ea4e389a feat: do not post "... verified" messages on QR scan success
We still post "... not verified" on failure.
2023-11-14 09:59:19 +00:00
link2xt
678142b3fb fix: assign MDNs to the trash chat early
Otherwise we will try to create an ad-hoc group
and failing because there are only two contacts
and then unblock a 1:1 chat just to assign
the message to trash in the end.
2023-11-14 08:59:37 +00:00
link2xt
ae6f83cd21 test: add a test checking that read receipt does not unblock 1:1 chat
Hidden 1:1 chat is created for the "chat protected" message.
It should not appear simply because we received
a read receipt from the contact in a group.
2023-11-14 08:59:37 +00:00
link2xt
626b2be1fe api(deltachat-rpc-client): add Account.get_chat_by_contact() 2023-11-14 08:59:37 +00:00
iequidoo
ac5c789c75 feat: Never drop better message from apply_group_changes()
Looks like this doesn't fix anything currently, because a better message from
`apply_group_changes()` doesn't appear in a context with another better message, but why drop it if
it's possible to add it, moreover, messages about implicit member additions are never dropped while
looking less important.
2023-11-14 03:11:30 -03:00
iequidoo
ce2878f1e8 test: If a message implicitly adds a group member, both messages appear (#4987) 2023-11-14 03:11:30 -03:00
link2xt
d4162899b4 fix: ignore special chats when calculating similar chats
The second SQL statement calculating chat size
was already fixed in f656cb29be,
but more important statement calculating member list intersection
was overlooked.
As a result, trash chat with members added there due to former bugs
could still appear in similar chats.
2023-11-13 23:32:52 +00:00
link2xt
e900d50e38 fix: allow to securejoin groups with 1:1 contact request from inviter 2023-11-13 18:24:20 +00:00
link2xt
a438a4746a chore(release): prepare for 1.131.1 2023-11-13 16:14:04 +00:00
link2xt
cfb819506f fix: do not skip actual message parts when group_changes_msg is inserted 2023-11-13 14:18:32 +00:00
link2xt
b86b915f40 chore(release): prepare for 1.131.0 2023-11-13 09:35:57 +00:00
iequidoo
ad5a5ad3db feat: Multi-device broadcast lists (#4953) 2023-11-13 05:00:57 -03:00
iequidoo
74081d8a36 feat: Sync Chat::name across devices (#4953) 2023-11-13 05:00:57 -03:00
iequidoo
34a434f07c refactor: Move chat-related code from sync to chat module
- Reduce cross-module dependencies.
- Stop bloating the `sync` module while implementing synchronisation of more entities.
- Now there's the only `ChatId` :)
2023-11-13 05:00:57 -03:00
iequidoo
dc944d8ca7 test: Broadcast lists synchronisation (#4953) 2023-11-13 05:00:57 -03:00
iequidoo
b06a7e7197 fix: Context::execute_sync_items: Ignore all errors (#4817)
An error while executing an item mustn't prevent next items from being executed. There was a comment
that only critical errors like db write failures must be reported upstack, but in fact it's hard to
achieve in the current design, there are no error codes or so, so it's bug-prone. E.g.
`ChatAction::Block` and `Unblock` already reported all errors upstack. So, let's make error handling
the same as everywhere and just ignore any errors in the item execution loop. In the worst case we
just do more unsuccessful db writes f.e.
2023-11-13 05:00:57 -03:00
iequidoo
fa61d90115 fix: Ignore errors from generating sync messages
It's sufficient if the local state is updated successfully, no need to fail the whole
operation. Anyway delivery of sync messages and applying them on other devices are beyond of our
control. If an error occurs when generating a sync messages, probably a log message is sufficient,
no need to even show it to a user. As for the tests, anyway there are ones on synchronisation which
perform necessary checks. Particularly, some sync messages can't be generated if an account is
unconfigured. Adding the corresponding checks to the device synchronisation code (and maybe even
more checks in the future) would complicate the code unnecessarily. Even errors caused by bugs in
this code aren't a reason to fail a local operation.
2023-11-13 05:00:57 -03:00
iequidoo
7977c9ab44 feat: Sync creating broadcast lists across devices (#4953) 2023-11-13 05:00:57 -03:00
iequidoo
6273a7d54e refactor: Rename Chat::add_sync_item() to sync() 2023-11-13 05:00:57 -03:00
iequidoo
4d1a9c2aa1 feat: Sync chat contacts across devices (#4953)
Sync chat contacts across devices for broadcast lists and groups. This needs the corresponding chat
to exist on other devices which is not the case for unpromoted groups, so it fails for them now but
it's only a warning and will work once creation of unpromoted groups is synchronised too.
2023-11-13 05:00:57 -03:00
Hocuri
b26ded423b Fix #4982: Allow to send unverified securejoin messages to protected chats 2023-11-12 20:15:38 +01:00
Hocuri
e4b6eba5d7 Correct comment 2023-11-12 20:15:38 +01:00
Hocuri
bc225024a1 Add tests 2023-11-12 20:15:38 +01:00
Hocuri
e616ecf160 Fix CI 2023-11-12 20:15:38 +01:00
Hocuri
f93562c6bf Refactorings 2023-11-12 20:15:38 +01:00
Hocuri
ac39c3699b Make sure the resetup happy path works 2023-11-12 20:15:38 +01:00
link2xt
091bc1ab13 fix: encode chat name in the List-ID header
List-ID header is added for broadcast lists.
UTF-8 in email headers is allowed only if
all recipient MTAs support SMTPUTF8 extension,
which is not always the case even if our submission service
reports SMTPUTF8 support.
2023-11-12 19:56:09 +01:00
link2xt
fcbb66a788 chore: update dependencies 2023-11-12 13:35:13 +00:00
link2xt
ab2bc3bfb2 chore: update dependencies 2023-11-10 21:28:28 +00:00
78 changed files with 2018 additions and 1774 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.73.0
RUSTUP_TOOLCHAIN: 1.74.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -76,11 +76,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.73.0
rust: 1.74.0
- os: windows-latest
rust: 1.73.0
rust: 1.74.0
- os: macos-latest
rust: 1.73.0
rust: 1.74.0
# Minimum Supported Rust Version = 1.70.0
- os: ubuntu-latest

View File

@@ -0,0 +1,25 @@
name: Build & Deploy Documentation on py.delta.chat
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Build Python documentation
run: scripts/build-python-docs.sh
- name: Upload to py.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: delta
KEY: ${{ secrets.CODESPEAK_KEY }}
HOST: "lists.codespeak.net"
SOURCE: "dist/html/"
TARGET: "/home/delta/build/master"

View File

@@ -1,5 +1,95 @@
# Changelog
## [1.131.5] - 2023-11-20
### API-Changes
- deltachat-rpc-client: Add `Message.get_sender_contact()`.
- Turn `ContactAddress` into an owned type.
### Features / Changes
- Lowercase addresses in Autocrypt and Autocrypt-Gossip headers.
- Lowercase the address in member added/removed messages.
- Lowercase `addr` when it is set.
- Do not replace the message with an error in square brackets when the sender is not a member of the protected group.
### Fixes
- `Chat::sync_contacts()`: Fetch contact addresses in a single query.
- `Chat::rename_ex()`: Sync improved chat name to other devices.
- Recognize `Chat-Group-Member-Added` of self case-insensitively.
- Compare verifier addr to peerstate addr case-insensitively.
### Tests
- Port [Secure-Join](https://securejoin.readthedocs.io/) tests to JSON-RPC.
### CI
- Test with Rust 1.74.
## [1.131.4] - 2023-11-16
### Documentation
- Document DC_DOWNLOAD_UNDECIPHERABLE.
### Fixes
- Always add "Member added" as system message.
## [1.131.3] - 2023-11-15
### Fixes
- Update async-imap to 0.9.4 which does not ignore EOF on FETCH.
- Reset gossiped timestamp on securejoin.
- sync: Ignore unknown sync items to provide forward compatibility and avoid creating empty message bubbles.
- sync: Skip sync when chat name is set to the current one.
- Return connectivity HTML with an error when IO is stopped.
## [1.131.2] - 2023-11-14
### API-Changes
- deltachat-rpc-client: add `Account.get_chat_by_contact()`.
### Features / Changes
- Do not post "... verified" messages on QR scan success.
- Never drop better message from `apply_group_changes()`.
### Fixes
- Assign MDNs to the trash chat early to prevent received MDNs from creating or unblocking 1:1 chats.
- Allow to securejoin groups when 1:1 chat with the inviter is a contact request.
- Add "setup changed" message for verified key before the message.
- Ignore special chats when calculating similar chats.
## [1.131.1] - 2023-11-13
### Fixes
- Do not skip actual message parts when group change messages are inserted.
## [1.131.0] - 2023-11-13
### Features / Changes
- Sync chat contacts across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
- Sync creating broadcast lists across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
- Sync Chat::name across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
- Multi-device broadcast lists ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
### Fixes
- Encode chat name in the `List-ID` header to avoid SMTPUTF8 errors.
- Ignore errors from generating sync messages.
- `Context::execute_sync_items`: Ignore all errors ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
- Allow to send unverified securejoin messages to protected chats ([#4982](https://github.com/deltachat/deltachat-core-rust/pull/4982)).
## [1.130.0] - 2023-11-10
### API-Changes
@@ -3156,3 +3246,9 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[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
[1.130.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.1...v1.130.0
[1.131.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.130.0...v1.131.0
[1.131.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.0...v1.131.1
[1.131.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.1...v1.131.2
[1.131.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.2...v1.131.3
[1.131.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.3...v1.131.4
[1.131.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.4...v1.131.5

View File

@@ -86,6 +86,11 @@ For example:
.with_context(|| format!("Unable to trash message {msg_id}"))
```
All errors should be handled in one of these ways:
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
- With `.log_err().ok()`.
- Bubbled up with `?`.
### Logging
For logging, use `info!`, `warn!` and `error!` macros.

251
Cargo.lock generated
View File

@@ -203,7 +203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e"
dependencies = [
"concurrent-queue",
"event-listener 3.0.1",
"event-listener 3.1.0",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
@@ -224,9 +224,9 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e542b1682eba6b85a721daef0c58e79319ffd0c678565c07ac57c8071c548b5"
checksum = "d736a74edf6c327b53dd9c932eae834253470ac5f0c55770e7e133bcbf986362"
dependencies = [
"async-channel 2.1.0",
"base64 0.21.5",
@@ -522,9 +522,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019"
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
dependencies = [
"memchr",
"serde",
@@ -585,9 +585,9 @@ dependencies = [
[[package]]
name = "cargo-platform"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12024c4645c97566567129c204f65d5815a8c9aecf30fcbe682b2fe034996d36"
checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff"
dependencies = [
"serde",
]
@@ -707,18 +707,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.7"
version = "4.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b"
checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.4.7"
version = "4.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663"
checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc"
dependencies = [
"anstyle",
"clap_lex",
@@ -931,9 +931,9 @@ dependencies = [
[[package]]
name = "crypto-bigint"
version = "0.5.3"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core 0.6.4",
@@ -1087,7 +1087,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.130.0"
version = "1.131.5"
dependencies = [
"ansi_term",
"anyhow",
@@ -1165,7 +1165,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.130.0"
version = "1.131.5"
dependencies = [
"anyhow",
"async-channel 2.1.0",
@@ -1189,7 +1189,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.130.0"
version = "1.131.5"
dependencies = [
"ansi_term",
"anyhow",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.130.0"
version = "1.131.5"
dependencies = [
"anyhow",
"deltachat",
@@ -1229,7 +1229,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.130.0"
version = "1.131.5"
dependencies = [
"anyhow",
"deltachat",
@@ -1479,15 +1479,15 @@ dependencies = [
[[package]]
name = "ecdsa"
version = "0.16.8"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der 0.7.8",
"digest 0.10.7",
"elliptic-curve 0.13.6",
"elliptic-curve 0.13.8",
"rfc6979 0.4.0",
"signature 2.1.0",
"signature 2.2.0",
"spki 0.7.2",
]
@@ -1508,7 +1508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8 0.10.2",
"signature 2.1.0",
"signature 2.2.0",
]
[[package]]
@@ -1528,14 +1528,15 @@ dependencies = [
[[package]]
name = "ed25519-dalek"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0"
dependencies = [
"curve25519-dalek 4.1.1",
"ed25519 2.2.3",
"serde",
"sha2 0.10.8",
"subtle",
"zeroize",
]
@@ -1578,12 +1579,12 @@ dependencies = [
[[package]]
name = "elliptic-curve"
version = "0.13.6"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct 0.2.0",
"crypto-bigint 0.5.3",
"crypto-bigint 0.5.5",
"digest 0.10.7",
"ff 0.13.0",
"generic-array",
@@ -1744,9 +1745,9 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [
"humantime",
"is-terminal",
@@ -1763,9 +1764,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.5"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
dependencies = [
"libc",
"windows-sys",
@@ -1798,9 +1799,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "3.0.1"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1"
checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2"
dependencies = [
"concurrent-queue",
"parking",
@@ -1813,15 +1814,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160"
dependencies = [
"event-listener 3.0.1",
"event-listener 3.1.0",
"pin-project-lite",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
@@ -1899,9 +1900,9 @@ dependencies = [
[[package]]
name = "fiat-crypto"
version = "0.2.2"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7"
[[package]]
name = "filetime"
@@ -2099,9 +2100,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
dependencies = [
"cfg-if",
"js-sys",
@@ -2150,9 +2151,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.21"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
dependencies = [
"bytes",
"fnv",
@@ -2160,7 +2161,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 1.9.3",
"indexmap",
"slab",
"tokio",
"tokio-util",
@@ -2173,12 +2174,6 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.2"
@@ -2195,7 +2190,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.14.2",
"hashbrown",
]
[[package]]
@@ -2301,9 +2296,9 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.9"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
dependencies = [
"bytes",
"fnv",
@@ -2472,16 +2467,6 @@ dependencies = [
"nom",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.1.0"
@@ -2489,7 +2474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [
"equivalent",
"hashbrown 0.14.2",
"hashbrown",
]
[[package]]
@@ -2679,9 +2664,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
dependencies = [
"cc",
"openssl-sys",
@@ -2697,9 +2682,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
[[package]]
name = "lock_api"
@@ -2834,7 +2819,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom 0.2.10",
"getrandom 0.2.11",
]
[[package]]
@@ -3175,8 +3160,8 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa 0.16.8",
"elliptic-curve 0.13.6",
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"primeorder",
"sha2 0.10.8",
]
@@ -3198,8 +3183,8 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
dependencies = [
"ecdsa 0.16.8",
"elliptic-curve 0.13.6",
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"primeorder",
"sha2 0.10.8",
]
@@ -3296,8 +3281,8 @@ dependencies = [
"derive_builder",
"des",
"digest 0.10.7",
"ed25519-dalek 2.0.0",
"elliptic-curve 0.13.6",
"ed25519-dalek 2.1.0",
"elliptic-curve 0.13.8",
"flate2",
"generic-array",
"hex",
@@ -3316,7 +3301,7 @@ dependencies = [
"sha1",
"sha2 0.10.8",
"sha3",
"signature 2.1.0",
"signature 2.2.0",
"smallvec",
"thiserror",
"twofish",
@@ -3516,11 +3501,11 @@ dependencies = [
[[package]]
name = "primeorder"
version = "0.13.3"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve 0.13.6",
"elliptic-curve 0.13.8",
]
[[package]]
@@ -3558,9 +3543,9 @@ dependencies = [
[[package]]
name = "proptest"
version = "1.3.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e"
checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf"
dependencies = [
"bitflags 2.4.1",
"lazy_static",
@@ -3568,7 +3553,7 @@ dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_xorshift",
"regex-syntax 0.7.5",
"regex-syntax 0.8.2",
"unarray",
]
@@ -3631,9 +3616,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c78e758510582acc40acb90458401172d41f1016f8c9dde89e49677afb7eec1"
checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a"
dependencies = [
"bytes",
"rand 0.8.5",
@@ -3744,7 +3729,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.10",
"getrandom 0.2.11",
]
[[package]]
@@ -3825,7 +3810,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
dependencies = [
"getrandom 0.2.10",
"getrandom 0.2.11",
"libredox",
"thiserror",
]
@@ -3868,12 +3853,6 @@ version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.2"
@@ -3971,7 +3950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b"
dependencies = [
"cc",
"getrandom 0.2.10",
"getrandom 0.2.11",
"libc",
"spin 0.9.8",
"untrusted 0.9.0",
@@ -4022,7 +4001,7 @@ dependencies = [
"pkcs1 0.7.5",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"signature 2.1.0",
"signature 2.2.0",
"spki 0.7.2",
"subtle",
"zeroize",
@@ -4030,9 +4009,9 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.29.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
dependencies = [
"bitflags 2.4.1",
"fallible-iterator",
@@ -4080,9 +4059,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.21"
version = "0.38.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
dependencies = [
"bitflags 2.4.1",
"errno",
@@ -4093,9 +4072,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.8"
version = "0.21.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9"
dependencies = [
"ring 0.17.5",
"rustls-webpki",
@@ -4116,9 +4095,9 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.5",
]
@@ -4204,9 +4183,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.15"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -4216,9 +4195,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.15"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
dependencies = [
"proc-macro2",
"quote",
@@ -4295,9 +4274,9 @@ dependencies = [
[[package]]
name = "self_cell"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6"
[[package]]
name = "semver"
@@ -4310,9 +4289,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.191"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a834c4821019838224821468552240d4d95d14e751986442c816572d39a080c9"
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
dependencies = [
"serde_derive",
]
@@ -4337,9 +4316,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.191"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fa52d5646bce91b680189fe5b1c049d2ea38dabb4e2e7c8d00ca12cfbfbcfd"
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
dependencies = [
"proc-macro2",
"quote",
@@ -4485,9 +4464,9 @@ dependencies = [
[[package]]
name = "signature"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest 0.10.7",
"rand_core 0.6.4",
@@ -4510,9 +4489,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.11.1"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "smawk"
@@ -4749,9 +4728,9 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449"
dependencies = [
"winapi-util",
]
@@ -4878,9 +4857,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.33.0"
version = "1.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
dependencies = [
"backtrace",
"bytes",
@@ -4907,9 +4886,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
@@ -5020,7 +4999,7 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap 2.1.0",
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
@@ -5100,9 +5079,9 @@ dependencies = [
[[package]]
name = "tracing-log"
version = "0.1.4"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
@@ -5111,9 +5090,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -5279,11 +5258,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.5.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [
"getrandom 0.2.10",
"getrandom 0.2.11",
"serde",
]
@@ -5700,18 +5679,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.7.25"
version = "0.7.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.25"
version = "0.7.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
dependencies = [
"proc-macro2",
"quote",
@@ -5720,9 +5699,9 @@ dependencies = [
[[package]]
name = "zeroize"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.130.0"
version = "1.131.5"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
@@ -72,7 +72,7 @@ quick-xml = "0.31"
rand = "0.8"
regex = "1.9"
reqwest = { version = "0.11.20", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rusqlite = { version = "0.30", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.5"
serde_json = "1.0"

View File

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

View File

@@ -2561,7 +2561,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* See https://securejoin.readthedocs.io/en/latest/new.html
* for details about both protocols.
* @return The text that should go to the QR code,
* On errors, an empty QR code is returned, NULL is never returned.
@@ -2597,7 +2597,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
*
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* See https://securejoin.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* @memberof dc_context_t
@@ -4580,15 +4580,18 @@ int dc_msg_has_html (dc_msg_t* msg);
* if they are larger than the limit set by the dc_set_config()-option `download_limit`.
*
* The function returns one of:
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
* and should be rendered as usual.
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
* In addition to the usual message rendering,
* the UI shall show a download button that calls dc_download_full_msg()
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
* If the download fails or succeeds,
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
* and should be rendered as usual.
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
* In addition to the usual message rendering,
* the UI shall show a download button that calls dc_download_full_msg()
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
* If the download fails or succeeds,
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
*
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any futher download action.
* It was fully downloaded, but we failed to decrypt it.
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
*
* @memberof dc_msg_t
* @param msg The message object.
@@ -6433,22 +6436,27 @@ void dc_event_unref(dc_event_t* event);
/**
* Download not needed, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_DONE 0
#define DC_DOWNLOAD_DONE 0
/**
* Download available, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_AVAILABLE 10
#define DC_DOWNLOAD_AVAILABLE 10
/**
* Download failed, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_FAILURE 20
#define DC_DOWNLOAD_FAILURE 20
/**
* Download not needed, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_UNDECIPHERABLE 30
/**
* Download in progress, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_IN_PROGRESS 1000
#define DC_DOWNLOAD_IN_PROGRESS 1000

View File

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

View File

@@ -678,7 +678,7 @@ impl CommandApi {
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
/// If not set, the Setup-Contact protocol is offered in the QR code.
/// See https://countermitm.readthedocs.io/en/latest/new.html
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// for details about both protocols.
///
/// return format: `[code, svg]`
@@ -707,7 +707,7 @@ impl CommandApi {
///
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
///
/// See https://countermitm.readthedocs.io/en/latest/new.html
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// for details about both protocols.
///
/// **qr**: The text of the scanned QR code. Typically, the same string as given

View File

@@ -53,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.130.0"
"version": "1.131.5"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.130.0"
version = "1.131.5"
license = "MPL-2.0"
edition = "2021"
@@ -11,7 +11,7 @@ deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.20"
pretty_env_logger = "0.5"
rusqlite = "0.29"
rusqlite = "0.30"
rustyline = "12"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }

0
deltachat-rpc-client/examples/echobot_advanced.py Normal file → Executable file
View File

8
deltachat-rpc-client/examples/echobot_no_hooks.py Normal file → Executable file
View File

@@ -40,13 +40,13 @@ def main():
while True:
event = account.wait_for_event()
if event["type"] == EventType.INFO:
if event["kind"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["type"] == EventType.WARNING:
elif event["kind"] == EventType.WARNING:
logging.warning("%s", event["msg"])
elif event["type"] == EventType.ERROR:
elif event["kind"] == EventType.ERROR:
logging.error("%s", event["msg"])
elif event["type"] == EventType.INCOMING_MSG:
elif event["kind"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
process_messages()

View File

@@ -111,6 +111,20 @@ class Account:
contacts = self._rpc.get_blocked_contacts(self.id)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
"""Return 1:1 chat for a contact if it exists."""
if isinstance(contact, Contact):
assert contact.account == self
contact_id = contact.id
elif isinstance(contact, int):
contact_id = contact
else:
raise ValueError(f"{contact!r} is not a contact")
chat_id = self._rpc.get_chat_id_by_contact_id(self.id, contact_id)
if chat_id:
return Chat(self, chat_id)
return None
def get_contacts(
self,
query: Optional[str] = None,
@@ -204,7 +218,7 @@ class Account:
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
:param qrdata: The text of the scanned QR code.
"""
@@ -257,6 +271,18 @@ class Account:
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_securejoin_inviter_success(self):
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
def wait_for_securejoin_joiner_success(self):
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
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

@@ -195,7 +195,7 @@ class Client:
class Bot(Client):
"""Simple bot implementation that listent to events of a single account."""
"""Simple bot implementation that listens to events of a single account."""
def configure(self, email: str, password: str, **kwargs) -> None:
kwargs.setdefault("bot", "1")

View File

@@ -42,6 +42,10 @@ class Message:
return AttrDict(reactions)
return None
def get_sender_contact(self) -> Contact:
from_id = self.get_snapshot().from_id
return self.account.get_contact_by_id(from_id)
def mark_seen(self) -> None:
"""Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id])

View File

@@ -9,20 +9,14 @@ def test_qr_setup_contact(acfactory) -> None:
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
alice.wait_for_securejoin_inviter_success()
# 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
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
@@ -35,24 +29,31 @@ def test_qr_securejoin(acfactory):
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("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
# Check that at least some of the handshake messages are deleted.
for ac in [alice, bob]:
while True:
event = ac.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
alice.wait_for_securejoin_inviter_success()
# 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
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
@@ -60,7 +61,97 @@ def test_qr_securejoin(acfactory):
assert bob_contact_alice_snapshot.is_verified
def test_verified_group_recovery(acfactory, rpc) -> None:
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
bob_chat_alice = snapshot.chat
assert bob_chat_alice.get_basic_snapshot().is_contact_request
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
# Chat stays being a contact request.
assert bob_chat_alice.get_basic_snapshot().is_contact_request
def test_qr_readreceipt(acfactory) -> None:
alice, bob, charlie = acfactory.get_online_accounts(3)
logging.info("Bob and Charlie setup contact with Alice")
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
charlie.secure_join(qr_code)
for joiner in [bob, charlie]:
joiner.wait_for_securejoin_joiner_success()
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
bob_addr = bob.get_config("addr")
charlie_addr = charlie.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
group.add_contact(alice_contact_bob)
group.add_contact(alice_contact_charlie)
# Promote a group.
group.send_message(text="Hello")
logging.info("Bob and Charlie receive a group")
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
bob_message = bob.get_message_by_id(bob_msg_id)
bob_snapshot = bob_message.get_snapshot()
assert bob_snapshot.text == "Hello"
# Charlie receives the same "Hello" message as Bob.
charlie.wait_for_incoming_msg_event()
logging.info("Bob sends a message to the group")
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
charlie_message = charlie.get_message_by_id(charlie_msg_id)
charlie_snapshot = charlie_message.get_snapshot()
assert charlie_snapshot.text == "Hi from Bob!"
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
assert not bob.get_chat_by_contact(bob_contact_charlie)
logging.info("Charlie reads Bob's message")
charlie_message.mark_seen()
while True:
event = bob.wait_for_event()
if event["kind"] == "MsgRead" and event["msg_id"] == bob_out_message.id:
break
# Receiving a read receipt from Charlie
# should not unblock hidden chat with Charlie for Bob.
assert not bob.get_chat_by_contact(bob_contact_charlie)
def test_verified_group_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
@@ -70,10 +161,7 @@ def test_verified_group_recovery(acfactory, rpc) -> None:
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.wait_for_securejoin_inviter_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
@@ -81,10 +169,7 @@ def test_verified_group_recovery(acfactory, rpc) -> None:
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
ac1.wait_for_securejoin_inviter_success()
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -93,69 +178,44 @@ def test_verified_group_recovery(acfactory, rpc) -> None:
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
ac3.wait_for_securejoin_inviter_success()
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)
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
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 snapshot.text == "Hi!"
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
assert ac1_contact.get_snapshot().is_verified
chat = Chat(ac2, chat_id)
chat.send_text("Works again!")
# ac2 can write messages to the group.
snapshot.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()
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).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!"
ac1_chat_messages = snapshot.chat.get_messages()
ac2_addr = ac2.get_config("addr")
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
# 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
ac1_chat_messages = snapshot.chat.get_messages()
ac2_addr = ac2.get_config("addr")
assert ac1_chat_messages[-1].get_snapshot().text == f"Changed setup for {ac2_addr}"
def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
@@ -165,10 +225,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
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.wait_for_securejoin_inviter_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
@@ -176,10 +233,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
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
ac1.wait_for_securejoin_inviter_success()
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -188,10 +242,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
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
ac3.wait_for_securejoin_inviter_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
@@ -247,3 +298,134 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
# 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_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
logging.info("ac3: verify with ac2")
qr_code, _svg = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
# 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
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if snapshot.text == "ac1 says hello":
assert snapshot.chat.get_basic_snapshot().is_protected
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.remove()
logging.info("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_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
# ensure ac2 receives message in VG
vg.send_text("hello")
while 1:
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if msg.text == "hello":
assert msg.chat.get_basic_snapshot().is_protected
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
qr_code, _svg = vg.get_qr_code()
ac4.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
while 1:
ev = ac2.wait_for_event()
if "added by unrelated SecureJoin" in str(ev):
return
def test_qr_new_group_unblocked(acfactory):
"""Regression test for a bug introduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining", protect=True)
qr_code, _svg = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac1_new_chat = ac1.create_group("Another group")
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
# Receive "Member added" message.
ac2.wait_for_incoming_msg_event()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
qr_code, _svg = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
logging.info("sending first message")
msg_out = chat.send_text("old address").get_snapshot()
logging.info("receiving first message")
ac2.wait_for_incoming_msg_event() # member added message
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert msg_in_1.text == msg_out.text
logging.info("changing email account")
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.stop_io()
ac1.configure()
ac1.start_io()
logging.info("sending second message")
msg_out = chat.send_text("changed address").get_snapshot()
logging.info("receiving second message")
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
msg_in_2_snapshot = msg_in_2.get_snapshot()
assert msg_in_2_snapshot.text == msg_out.text
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
assert ac1new.get_config("addr") in [
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
]

View File

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

View File

@@ -27,8 +27,6 @@ skip = [
{ name = "ed25519", version = "1.5.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "getrandom", version = "<0.2" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "indexmap", version = "<2.0.0" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },

View File

@@ -28,6 +28,7 @@ module.exports = {
DC_DOWNLOAD_DONE: 0,
DC_DOWNLOAD_FAILURE: 20,
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_DOWNLOAD_UNDECIPHERABLE: 30,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
DC_EVENT_CHAT_MODIFIED: 2020,
DC_EVENT_CONFIGURE_PROGRESS: 2041,

View File

@@ -28,6 +28,7 @@ export enum C {
DC_DOWNLOAD_DONE = 0,
DC_DOWNLOAD_FAILURE = 20,
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_DOWNLOAD_UNDECIPHERABLE = 30,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
DC_EVENT_CHAT_MODIFIED = 2020,
DC_EVENT_CONFIGURE_PROGRESS = 2041,

View File

@@ -56,5 +56,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.130.0"
"version": "1.131.5"
}

View File

@@ -1,6 +1,6 @@
=========================
DeltaChat Python bindings
=========================
============================
CFFI Python Bindings
============================
This package provides `Python bindings`_ to the `deltachat-core library`_
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
@@ -8,157 +8,3 @@ a low-level Chat/Contact/Message API to user interfaces and bots.
.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust
.. _`Python bindings`: https://py.delta.chat/
Installing pre-built packages (Linux-only)
==========================================
If you have a Linux system you may install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself`__.
__ sourceinstall_
We recommend to first create a fresh Python virtual environment
and activate it in your shell::
python -m venv env
source env/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``env`` directory and leaves
your system installation alone.
For Linux we build wheels for all releases and push them to a python package
index. To install the latest release::
pip install deltachat
To verify it worked::
python -c "import deltachat"
Running tests
=============
Recommended way to run tests is using `scripts/run-python-test.sh`
script provided in the core repository.
This script compiles the library in debug mode and runs the tests using `tox`_.
By default it will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real e-mail servers.
.. _`tox`: https://tox.wiki
.. _livetests:
Running "live" tests with temporary accounts
--------------------------------------------
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::
export CHATMAIL_DOMAIN=nine.testrun.org
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::
tox -e py
Each test run creates new accounts.
Developing the bindings
-----------------------
If you want to develop or debug the bindings,
you can create a testing development environment using `tox`::
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=debug
tox -c python --devenv env -e py
. env/bin/activate
Inside this environment the bindings are installed
in editable mode (as if installed with `python -m pip install -e`)
together with the testing dependencies like `pytest` and its plugins.
You can then edit the source code in the development tree
and quickly run `pytest` manually without waiting for `tox`
to recreating the virtual environment each time.
.. _sourceinstall:
Installing bindings from source
===============================
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then clone the deltachat-core-rust repo::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
First, build the core library::
cargo build --release -p deltachat_ffi --features jsonrpc
`jsonrpc` feature is required even if not used by the bindings
because `deltachat.h` includes JSON-RPC functions unconditionally.
Create the virtual environment and activate it:
python -m venv env
source env/bin/activate
Build and install the bindings:
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=release
python -m pip install ./python
`DCC_RS_DEV` environment variable specifies the location of
the core development tree. If this variable is not set,
`libdeltachat` library and `deltachat.h` header are expected
to be installed system-wide.
When `DCC_RS_DEV` is set, `DCC_RS_TARGET` specifies
the build profile name to look up the artifacts
in the target directory.
In this case setting it can be skipped because
`DCC_RS_TARGET=release` is the default.
Building manylinux based wheels
===============================
Building portable manylinux wheels which come with libdeltachat.so
can be done with Docker_ or Podman_.
.. _Docker: https://www.docker.com/
.. _Podman: https://podman.io/
If you want to build your own wheels, build container image first::
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
$ docker build -t deltachat/coredeps scripts/coredeps
This will use the ``scripts/coredeps/Dockerfile`` to build
container image called ``deltachat/coredeps``. You can afterwards
find it with::
$ docker images
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e CHATMAIL_DOMAIN \
--rm -it -v $(pwd):/mnt -w /mnt \
deltachat/coredeps scripts/run_all.sh

View File

@@ -1,197 +0,0 @@
# Makefile for Sphinx documentation
#
VERSION = $(shell python -c "import conf ; print(conf.version)")
DOCZIP = devpi-$(VERSION).doc.zip
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
RSYNCOPTS = -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
export HOME=/tmp/home
export TESTHOME=$(HOME)
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# This variable is not auto generated as the order is important.
USER_MAN_CHAPTERS = commands\
user\
indices\
packages\
# userman/index.rst\
# userman/devpi_misc.rst\
# userman/devpi_concepts.rst\
#export DEVPI_CLIENTDIR=$(CURDIR)/.tmp_devpi_user_man/client
#export DEVPI_SERVERDIR=$(CURDIR)/.tmp_devpi_user_man/server
chapter = commands
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp \
epub latex latexpdf text man changes linkcheck doctest gettext install \
quickstart-releaseprocess quickstart-pypimirror quickstart-server regen \
prepare-quickstart\
regen.server-fresh regen.server-restart regen.server-clean\
regen.uman-all regen.uman
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo
@echo "User Manual Regen Targets"
@echo " regen.uman regenerates page. of the user manual chapeter e.g. regen.uman chapter=..."
@echo " regen.uman-all regenerates the user manual"
@echo " regen.uman-clean stop temp server and clean up directory"
@echo " Chapter List: $(USER_MAN_CHAPTERS)"
clean:
-rm -rf $(BUILDDIR)/*
version:
@echo "version $(VERSION)"
doczip: html
python doczip.py $(DOCZIP) _build/html
install: html
rsync -avz $(RSYNCOPTS) _build/html/ delta@py.delta.chat:build/master
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/devpi.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/devpi.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/devpi"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/devpi"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@@ -1,17 +0,0 @@
<div class="globaltoc">
<ul>
<li><a href="{{ pathto('index') }}">index</a></li>
<li><a href="{{ pathto('install') }}">install</a></li>
<li><a href="{{ pathto('api') }}">high level API</a></li>
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
</ul>
<b>external links:</b>
<ul>
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
<li><a href="https://web.libera.chat/#deltachat">#deltachat</a></li>
</ul>
</div>

View File

@@ -1 +0,0 @@
<h3>deltachat {{release}}</h3>

View File

@@ -1,5 +1,4 @@
high level API reference
High Level API Reference
========================
- :class:`deltachat.Account` (your main entry point, creates the
@@ -8,28 +7,14 @@ high level API reference
- :class:`deltachat.Chat`
- :class:`deltachat.Message`
Account
-------
.. autoclass:: deltachat.Account
:members:
Contact
-------
:members:
.. autoclass:: deltachat.Contact
:members:
Chat
----
:members:
.. autoclass:: deltachat.Chat
:members:
Message
-------
:members:
.. autoclass:: deltachat.Message
:members:
:members:

View File

@@ -1,11 +1,10 @@
examples
Examples
========
Once you have :doc:`installed deltachat bindings <install>`
you need email/password credentials for an IMAP/SMTP account.
Delta Chat developers and the CI system use a special URL to create
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
temporary email accounts on `testrun.org <https://testrun.org/>`_ for testing.
Receiving a Chat message from the command line
----------------------------------------------
@@ -16,11 +15,11 @@ Here is a simple bot that:
- terminates the bot if the message `/quit` is sent
.. include:: ../examples/echo_and_quit.py
.. include:: ../../examples/echo_and_quit.py
:literal:
With this file in your working directory you can run the bot
by specifying a database path, an e-mail address and password of
by specifying a database path, an email address and password of
a SMTP-IMAP account::
$ cd examples
@@ -40,11 +39,11 @@ Here is a simple bot that:
- tracks member additions and removals for all chat groups
.. include:: ../examples/group_tracking.py
.. include:: ../../examples/group_tracking.py
:literal:
With this file in your working directory you can run the bot
by specifying a database path, an e-mail address and password of
by specifying a database path, an email address and password of
a SMTP-IMAP account::
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db

View File

@@ -0,0 +1,80 @@
Install
=======
Installing pre-built packages (Linux-only)
------------------------------------------
If you have a Linux system you may install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself`__.
__ sourceinstall_
We recommend to first create a fresh Python virtual environment
and activate it in your shell::
python -m venv env
source env/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``env`` directory and leaves
your system installation alone.
For Linux we build wheels for all releases and push them to a python package
index. To install the latest release::
pip install deltachat
To verify it worked::
python -c "import deltachat"
.. _sourceinstall:
Installing bindings from source
-------------------------------
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then clone the deltachat-core-rust repo::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
First, build the core library::
cargo build --release -p deltachat_ffi --features jsonrpc
`jsonrpc` feature is required even if not used by the bindings
because `deltachat.h` includes JSON-RPC functions unconditionally.
Create the virtual environment and activate it::
python -m venv env
source env/bin/activate
Build and install the bindings::
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=release
python -m pip install ./python
`DCC_RS_DEV` environment variable specifies the location of
the core development tree. If this variable is not set,
`libdeltachat` library and `deltachat.h` header are expected
to be installed system-wide.
When `DCC_RS_DEV` is set, `DCC_RS_TARGET` specifies
the build profile name to look up the artifacts
in the target directory.
In this case setting it can be skipped because
`DCC_RS_TARGET=release` is the default.

11
python/doc/cffi/intro.rst Normal file
View File

@@ -0,0 +1,11 @@
Introduction
============
CFFI bindings are available via the `deltachat <https://pypi.org/project/deltachat/>`_ Python package.
The package contains both the Python bindings and the Delta Chat core.
It is provided only for Linux.
The ``deltachat`` Python package provides two layers of bindings for the
core Rust-library of the https://delta.chat messaging ecosystem:
low-level CFFI bindings to the C interface of the Delta Chat core
and high-level Python bindings built on top of CFFI bindings.

View File

@@ -1,8 +1,7 @@
Low Level API Reference
=======================
low level API reference
===================================
for full doxygen-generated C-docs, defines and functions please checkout
For full doxygen-generated C-docs, defines and functions please checkout
https://c.delta.chat

View File

@@ -0,0 +1,25 @@
Building Manylinux-Based Wheels
===============================
Building portable manylinux wheels which come with libdeltachat.so
can be done with Docker_ or Podman_.
.. _Docker: https://www.docker.com/
.. _Podman: https://podman.io/
If you want to build your own wheels, build container image first::
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
$ docker build -t deltachat/coredeps scripts/coredeps
This will use the ``scripts/coredeps/Dockerfile`` to build
container image called ``deltachat/coredeps``. You can afterwards
find it with::
$ docker images
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e CHATMAIL_DOMAIN \
--rm -it -v $(pwd):/mnt -w /mnt \
deltachat/coredeps scripts/run_all.sh

49
python/doc/cffi/tests.rst Normal file
View File

@@ -0,0 +1,49 @@
Running Tests
=============
Recommended way to run tests is using `scripts/run-python-test.sh`
script provided in the core repository.
This script compiles the library in debug mode and runs the tests using `tox`_.
By default it will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real email servers.
.. _`tox`: https://tox.wiki
.. _livetests:
Running "Live" Tests With Temporary Accounts
--------------------------------------------
If you want to run live functional tests
you can set ``CHATMAIL_DOMAIN`` to a domain of the email server
that creates email accounts like this::
export CHATMAIL_DOMAIN=nine.testrun.org
With this account-creation setting, pytest runs create ephemeral email 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::
tox -e py
Each test run creates new accounts.
Developing the Bindings
-----------------------
If you want to develop or debug the bindings,
you can create a testing development environment using `tox`::
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=debug
tox -c python --devenv env -e py
. env/bin/activate
Inside this environment the bindings are installed
in editable mode (as if installed with `python -m pip install -e`)
together with the testing dependencies like `pytest` and its plugins.
You can then edit the source code in the development tree
and quickly run `pytest` manually without waiting for `tox`
to recreating the virtual environment each time.

View File

@@ -1,4 +0,0 @@
Changelog for deltachat-core's Python bindings
==============================================
.. include:: ../CHANGELOG

View File

@@ -1,138 +1,94 @@
# -*- coding: utf-8 -*-
#
# devpi documentation build configuration file, created by
# sphinx-quickstart on Mon Jun 3 16:11:22 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
from pathlib import Path
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
from deltachat import __version__ as release
version = ".".join(release.split(".")[:2])
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
#'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.viewcode',
'breathe',
#'sphinx.ext.githubpages',
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.viewcode",
"breathe",
"sphinx_rtd_theme",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'deltachat'
copyright = u'2020, holger krekel and contributors'
project = "Delta Chat"
copyright = "2023, Delta Chat contributors"
author = "Delta Chat contributors"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['sketch', '_build', "attic"]
exclude_patterns = ["sketch", "_build", "attic", "Thumbs.db", ".DS_Store"]
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# -- breathe options ------
breathe_projects = {
"deltachat": "../../docs/xml/"
}
breathe_projects = {"deltachat": Path("../../docs/xml/")}
breathe_default_project = "deltachat"
# -- Options for HTML output ---------------------------------------------------
sys.path.append(os.path.abspath('_themes'))
html_theme_path = ['_themes']
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# html_theme = 'flask'
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
'logo': '_static/delta-chat.svg',
'font_size': "1.1em",
'caption_font_size': "0.9em",
'code_font_size': "1.1em",
}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = ["_themes"]
html_theme = "sphinx_rtd_theme"
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
@@ -141,51 +97,34 @@ html_logo = "_static/delta-chat.svg"
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = '_static/favicon.ico'
html_favicon = "_static/favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
html_sidebars = {
'index': [
'sidebarintro.html',
'globaltoc.html',
'searchbox.html'
],
'**': [
'sidebarintro.html',
'globaltoc.html',
'relations.html',
'searchbox.html'
]
}
# html_use_smartypants = True
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
@@ -194,71 +133,65 @@ html_show_sourcelink = False
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
html_use_opensearch = 'https://doc.devpi.net'
html_use_opensearch = "https://doc.devpi.net"
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'deltachat-python'
htmlhelp_basename = "deltachat-python"
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
'pointsize': '12pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '12pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'devpi.tex', u'deltachat documentation',
u'holger krekel', 'manual'),
("index", "devpi.tex", "deltachat documentation", "holger krekel", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'deltachat', u'deltachat documentation',
[u'holger krekel'], 1)
]
man_pages = [("index", "deltachat", "deltachat documentation", ["holger krekel"], 1)]
# If true, show URL addresses after external links.
#man_show_urls = False
# man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
@@ -267,30 +200,38 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'devpi', u'devpi Documentation',
u'holger krekel', 'devpi', 'One line description of project.',
'Miscellaneous'),
(
"index",
"devpi",
"devpi Documentation",
"holger krekel",
"devpi",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# texinfo_show_urls = 'footnote'
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
intersphinx_mapping = {"http://docs.python.org/": None}
# autodoc options
autodoc_member_order = "bysource"
# always document __init__ functions
def skip(app, what, name, obj, skip, options):
return skip
def setup(app):
app.connect("autodoc-skip-member", skip)

View File

@@ -1,41 +1,44 @@
deltachat python bindings
=========================
Delta Chat Python bindings, new and old
=======
The ``deltachat`` Python package provides two layers of bindings for the
core Rust-library of the https://delta.chat messaging ecosystem:
- :doc:`api` is a high level interface to deltachat-core.
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
<https://github.com/deltachat/deltachat-core-rust>`_.
getting started
---------------
`Delta Chat <https://delta.chat/>`_ provides two kinds of Python bindings
to the `Rust Core <https://github.com/deltachat/deltachat-core-rust>`_:
JSON-RPC bindings and CFFI bindings.
When starting a new project it is recommended to use JSON-RPC bindings,
which are used in the Delta Chat Desktop app through generated Typescript-bindings.
The Python JSON-RPC bindings are maintained by Delta Chat core developers.
Most existing bot projects and many tests in Delta Chat's own core library
still use the CFFI-bindings, and it is going to be maintained certainly also in 2024.
New APIs might however only appear in the JSON-RPC bindings,
as the CFFI bindings are increasingly in maintenance-only mode.
.. toctree::
:maxdepth: 2
:caption: JSON-RPC Bindings
install
examples
jsonrpc/intro
jsonrpc/install
jsonrpc/examples
jsonrpc/reference
jsonrpc/develop
.. toctree::
:hidden:
:maxdepth: 2
:caption: CFFI Bindings
links
changelog
api
lapi
plugins
..
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
cffi/intro
cffi/install
cffi/examples
cffi/manylinux
cffi/tests
cffi/api
cffi/lapi
cffi/plugins
.. _`deltachat`: https://delta.chat
.. _`deltachat-core repo`: https://github.com/deltachat
.. _pip: http://pypi.org/project/pip/
.. _virtualenv: http://pypi.org/project/virtualenv/
.. _merlinux: http://merlinux.eu
.. _pypi: http://pypi.org/
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core-rust

View File

@@ -1,2 +0,0 @@
.. include:: ../README.rst

View File

@@ -0,0 +1,68 @@
===========
Development
===========
To develop JSON-RPC bindings,
clone the `deltachat-core-rust <https://github.com/deltachat/deltachat-core-rust/>`_ repository::
git clone https://github.com/deltachat/deltachat-core-rust.git
Testing
=======
To run online tests, set ``CHATMAIL_DOMAIN``
to a domain of the email server
that can be used to create testing accounts::
export CHATMAIL_DOMAIN=nine.testrun.org
Then run ``scripts/run-rpc-test.sh``
to build debug version of ``deltachat-rpc-server``
and run ``deltachat-rpc-client`` tests
in a separate virtual environment managed by `tox <https://tox.wiki/>`_.
Development Environment
=======================
Creating a new virtual environment
to run the tests each time
as ``scripts/run-rpc-test.sh`` does is slow
if you are changing the tests or the code
and want to rerun the tests each time.
If you are developing the tests,
it is better to create a persistent virtual environment.
You can do this by running ``scripts/make-rpc-testenv.sh``.
This creates a virtual environment ``venv`` which you can then enter with::
. venv/bin/activate
Then you can run the tests with
::
pytest deltachat-rpc-client/tests/
Refer to `pytest documentation <https://docs.pytest.org/>` for details.
If make the changes to Delta Chat core
or Python bindings, you can rebuild the environment by rerunning
``scripts/make-rpc-testenv.sh``.
It is ok to rebuild the activated environment this way,
you do not need to deactivate or reactivate the environment each time.
Using REPL
==========
Once you have a development environment,
you can quickly test things in REPL::
$ python
>>> from deltachat_rpc_client import *
>>> rpc = Rpc()
>>> rpc.start()
>>> dc = DeltaChat(rpc)
>>> system_info = dc.get_system_info()
>>> system_info["level"]
'awesome'
>>> rpc.close()

View File

@@ -0,0 +1,19 @@
Examples
========
Echo bot
--------
.. include:: ../../../deltachat-rpc-client/examples/echobot_no_hooks.py
:literal:
Echo bot with hooks
-------------------
.. include:: ../../../deltachat-rpc-client/examples/echobot.py
:literal:
Advanced echo bot
-----------------
.. include:: ../../../deltachat-rpc-client/examples/echobot_advanced.py
:literal:

View File

@@ -0,0 +1,36 @@
Install
=======
To use JSON-RPC bindings for Delta Chat core you will need
a ``deltachat-rpc-server`` binary which provides Delta Chat core API over JSON-RPC
and a ``deltachat-rpc-client`` Python package which is a JSON-RPC client that starts ``deltachat-rpc-server`` process and uses JSON-RPC API.
`Create a virtual environment <https://docs.python.org/3/library/venv.html>`__ if you
dont have one already and activate it::
$ python -m venv venv
$ . venv/bin/activate
Install ``deltachat-rpc-server``
--------------------------------
To get ``deltachat-rpc-server`` binary you have three options:
1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``.
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server``.
3. Download prebuilt release from https://github.com/deltachat/deltachat-core-rust/releases and install it into ``PATH``.
Check that ``deltachat-rpc-server`` is installed and can run::
$ deltachat-rpc-server --version
1.131.4
Then install ``deltachat-rpc-client`` with ``pip install deltachat-rpc-client``.
Install ``deltachat-rpc-client``
--------------------------------
To get ``deltachat-rpc-client`` Python library you can:
1. Install ``deltachat-rpc-client`` from PyPI using ``pip install deltachat-rpc-client``.
2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/deltachat/deltachat-core-rust.git@main#subdirectory=deltachat-rpc-client``.

View File

@@ -0,0 +1,8 @@
Introduction
============
JSON-RPC bindings are available via the `deltachat-rpc-client <https://pypi.org/project/deltachat-rpc-client/>`_ Python package.
This package provides only the Python bindings and requires ``deltachat-rpc-server`` binary to be installed.
`deltachat-rpc-server <https://pypi.org/project/deltachat-rpc-server/>`_ package provides ``deltachat-rpc-server`` binary for Linux, Windows, macOS and Android.
RPC client connects to standalone Delta Chat RPC server ``deltachat-rpc-server`` and provides Python interface to it.

View File

@@ -0,0 +1,5 @@
API Reference
=============
.. automodule:: deltachat_rpc_client
:members:

View File

@@ -1,11 +0,0 @@
links
================================
.. _`deltachat`: https://delta.chat
.. _`deltachat-core repo`: https://github.com/deltachat
.. _pip: http://pypi.org/project/pip/
.. _virtualenv: http://pypi.org/project/virtualenv/
.. _merlinux: http://merlinux.eu
.. _pypi: http://pypi.org/
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core

View File

@@ -1,190 +0,0 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\devpi.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\devpi.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

View File

@@ -140,6 +140,10 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_system_message()
assert "added" in msg.text.lower()
assert any(
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
for m in msg.chat.get_messages()
)
lp.sec("ac1: send message")
msg_out = chat1.send_text("hello")
assert msg_out.is_encrypted()
@@ -580,6 +584,7 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
lp.sec("ac2_offl: sending message")
msg_out = chat2.send_text("hello")
@@ -647,6 +652,15 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
chat2.send_text("hi2")
lp.sec("ac2_offl: receiving message")
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert msg_in.is_system_message()
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
# We need to consume one event that has data2=0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev.data2 == 0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert not msg_in.is_system_message()

View File

@@ -9,7 +9,7 @@ import pytest
from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message, Chat
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
@@ -1658,128 +1658,6 @@ def test_ac_setup_message_twice(acfactory, lp):
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
def test_qr_setup_contact(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
qr = ac1.get_setup_contact_qr()
lp.sec("ac2: start QR-code based setup contact protocol")
ch = ac2.qr_setup_contact(qr)
assert ch.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
ch = ac2.qr_join_chat(qr)
lp.sec("ac2: qr_join_chat() returned")
assert ch.id >= 10
# check that at least some of the handshake messages are deleted
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac1._evtracker.wait_securejoin_inviter_progress(1000)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
# ac1 reloads the chat.
chat = Chat(chat.account, chat.id)
assert not chat.is_protected()
# ac2 reloads the chat.
ch = Chat(ch.account, ch.id)
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.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
qr = ac1_chat.get_join_qr()
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
ac1_new_chat = ac1.create_group_chat("Another group")
ac1_new_chat.add_contact(ac2)
# Receive "Member added" message.
ac2._evtracker.wait_next_incoming_message()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
@@ -2527,47 +2405,6 @@ def test_delete_deltachat_folder(acfactory):
assert "DeltaChat" in ac1.direct_imap.list_folders()
def test_aeap_flow_verified(acfactory, lp):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
assert chat2.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("sending first message")
msg_out = chat.send_text("old address")
lp.sec("receiving first message")
ac2._evtracker.wait_next_incoming_message() # member added message
msg_in_1 = ac2._evtracker.wait_next_incoming_message()
assert msg_in_1.text == msg_out.text
lp.sec("changing email account")
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.stop_io()
configtracker = ac1.configure()
configtracker.wait_finish()
ac1.start_io()
lp.sec("sending second message")
msg_out = chat.send_text("changed address")
lp.sec("receiving second message")
msg_in_2 = ac2._evtracker.wait_next_incoming_message()
assert msg_in_2.text == msg_out.text
assert msg_in_2.chat.id == msg_in_1.chat.id
assert msg_in_2.get_sender_contact().addr == ac1new.get_config("addr")
assert len(msg_in_2.chat.get_contacts()) == 2
assert ac1new.get_config("addr") in [contact.addr for contact in msg_in_2.chat.get_contacts()]
def test_archived_muted_chat(acfactory, lp):
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.

View File

@@ -62,9 +62,9 @@ commands =
[testenv:doc]
changedir=doc
deps =
# Pinned due to incompatibility of breathe with sphinx 7.2: <https://github.com/breathe-doc/breathe/issues/943>
sphinx<=7.1.2
sphinx
breathe
sphinx_rtd_theme
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html

View File

@@ -1 +1 @@
2023-11-10
2023-11-20

View File

@@ -39,6 +39,8 @@ and an own build machine.
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
- `build-python-docs.sh` builds Python documentation into `dist/html/`.
## Triggering runs on the build machine locally (fast!)
There is experimental support for triggering a remote Python or Rust test run

13
scripts/build-python-docs.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
python3 -m venv venv
venv/bin/pip install ./python
venv/bin/pip install ./deltachat-rpc-client
venv/bin/pip install sphinx breathe sphinx_rtd_theme
venv/bin/pip install ./deltachat-rpc-client
venv/bin/sphinx-build -b html -a python/doc/ dist/html

View File

@@ -102,8 +102,6 @@ jobs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-docs
path: ./python/doc/_build/
# Binary wheels
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
@@ -115,28 +113,6 @@ jobs:
- |
scripts/run_all.sh
# Upload python docs to py.delta.chat
- task: upload-py-docs
config:
inputs:
- name: py-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete py-docs/html/ delta@py.delta.chat:build/master
# Upload x86_64 wheels and source packages
- task: upload-wheels
config:

View File

@@ -1,6 +1,6 @@
#!/bin/sh
#
# Build the Delta Chat Core Rust library, Python wheels and docs
# Build the Delta Chat Core Rust library and Python wheels
set -e -x
@@ -34,9 +34,3 @@ unset CHATMAIL_DOMAIN
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
echo -----------------------
echo generating python docs
echo -----------------------
tox --workdir "$TOXWORKDIR" -e doc

View File

@@ -67,7 +67,7 @@ impl Aheader {
impl fmt::Display for Aheader {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "addr={};", self.addr)?;
write!(fmt, "addr={};", self.addr.to_lowercase())?;
if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?;
}
@@ -262,5 +262,16 @@ mod tests {
)
)
.contains("prefer-encrypt"));
// Always lowercase the address in the header.
assert!(format!(
"{}",
Aheader::new(
"TeSt@eXaMpLe.cOm".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
)
.contains("test@example.com"));
}
}

View File

@@ -1,13 +1,13 @@
//! # Chat module.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
@@ -21,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::{self, Contact, ContactId, Origin, VerifiedStatus};
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
@@ -29,6 +29,7 @@ use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::location;
use crate::log::LogExt;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -38,11 +39,11 @@ use crate::receive_imf::ReceivedMsg;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
use crate::stock_str;
use crate::sync::{self, ChatAction, Sync::*, SyncData};
use crate::sync::{self, 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,
strip_rtlo_characters, time, IsNoneOrEmpty,
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
@@ -399,7 +400,10 @@ 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?;
chat.sync(context, SyncAction::Block)
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -417,7 +421,10 @@ impl ChatId {
// 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?;
chat.sync(context, SyncAction::Unblock)
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -465,7 +472,10 @@ impl ChatId {
}
if sync.into() {
chat.add_sync_item(context, ChatAction::Accept).await?;
chat.sync(context, SyncAction::Accept)
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -567,6 +577,28 @@ impl ChatId {
}
}
/// Sets the 1:1 chat with the given address to ProtectionStatus::Protected,
/// and posts a `SystemMessage::ChatProtectionEnabled` into it.
///
/// If necessary, creates a hidden chat for this.
pub(crate) async fn set_protection_for_contact(
context: &Context,
contact_id: ContactId,
) -> Result<()> {
let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
.await
.with_context(|| format!("can't create chat for {}", contact_id))?;
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
smeared_time(context),
Some(contact_id),
)
.await?;
Ok(())
}
/// Archives or unarchives a chat.
pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
self.set_visibility_ex(context, Sync, visibility).await
@@ -605,8 +637,10 @@ impl ChatId {
if sync.into() {
let chat = Chat::load_from_db(context, self).await?;
chat.add_sync_item(context, ChatAction::SetVisibility(visibility))
.await?;
chat.sync(context, SyncAction::SetVisibility(visibility))
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -959,8 +993,9 @@ impl ChatId {
AND y.contact_id > 9
AND x.chat_id=?
AND y.chat_id<>x.chat_id
AND y.chat_id>?
GROUP BY y.chat_id",
(self,),
(self, DC_CHAT_ID_LAST_SPECIAL),
|row| {
let chat_id: ChatId = row.get(0)?;
let intersection: f64 = row.get(1)?;
@@ -1697,7 +1732,11 @@ impl Chat {
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
// when the group-creation message is actually sent though SMTP -
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
context.sync_qr_code_tokens(Some(self.id)).await?;
context
.sync_qr_code_tokens(Some(self.id))
.await
.log_err(context)
.ok();
}
// reset encrypt error state eg. for forwarding
@@ -1894,8 +1933,25 @@ impl Chat {
Ok(msg.id)
}
/// Sends a `SyncAction` synchronising chat contacts to other devices.
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
let addrs = context
.sql
.query_map(
"SELECT c.addr \
FROM contacts c INNER JOIN chats_contacts cc \
ON c.id=cc.contact_id \
WHERE cc.chat_id=?",
(self.id,),
|row| row.get::<_, String>(0),
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
self.sync(context, SyncAction::SetContacts(addrs)).await
}
/// Returns chat id for the purpose of synchronisation across devices.
async fn get_sync_id(&self, context: &Context) -> Result<Option<sync::ChatId>> {
async fn get_sync_id(&self, context: &Context) -> Result<Option<SyncId>> {
match self.typ {
Chattype::Single => {
let mut r = None;
@@ -1907,7 +1963,7 @@ impl Chat {
return Ok(None);
}
let contact = Contact::get_by_id(context, contact_id).await?;
r = Some(sync::ChatId::ContactAddr(contact.get_addr().to_string()));
r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
}
Ok(r)
}
@@ -1915,23 +1971,28 @@ impl Chat {
if self.grpid.is_empty() {
return Ok(None);
}
Ok(Some(sync::ChatId::Grpid(self.grpid.clone())))
Ok(Some(SyncId::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<()> {
/// Synchronises a chat action to other devices.
pub(crate) async fn sync(&self, context: &Context, action: SyncAction) -> 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?;
sync(context, id, action).await?;
}
Ok(())
}
}
async fn sync(context: &Context, id: SyncId, action: SyncAction) -> Result<()> {
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, EnumIter)]
#[repr(i8)]
@@ -2386,10 +2447,13 @@ async fn prepare_msg_common(
// Check if the chat can be sent to.
if let Some(reason) = chat.why_cant_send(context).await? {
if reason == CantSendReason::ProtectionBroken
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
if matches!(
reason,
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest
) && msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
// Send out the message, the securejoin message is supposed to repair the verification
// Send out the message, the securejoin message is supposed to repair the verification.
// If the chat is a contact request, let the user accept it later.
} else {
bail!("cannot send to {chat_id}: {reason}");
}
@@ -3193,26 +3257,79 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
let chat_name = find_unused_broadcast_list_name(context).await?;
let grpid = create_id();
let row_id = context
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(
Chattype::Broadcast,
chat_name,
grpid,
create_smeared_timestamp(context),
),
)
.await?;
create_broadcast_list_ex(context, Sync, grpid, chat_name).await
}
pub(crate) async fn create_broadcast_list_ex(
context: &Context,
sync: sync::Sync,
grpid: String,
chat_name: String,
) -> Result<ChatId> {
let row_id = {
let chat_name = &chat_name;
let grpid = &grpid;
let trans_fn = |t: &mut rusqlite::Transaction| {
let cnt = t.execute("UPDATE chats SET name=? WHERE grpid=?", (chat_name, grpid))?;
ensure!(cnt <= 1, "{cnt} chats exist with grpid {grpid}");
if cnt == 1 {
return Ok(t.query_row(
"SELECT id FROM chats WHERE grpid=? AND type=?",
(grpid, Chattype::Broadcast),
|row| {
let id: isize = row.get(0)?;
Ok(id)
},
)?);
}
t.execute(
"INSERT INTO chats \
(type, name, grpid, param, created_timestamp) \
VALUES(?, ?, ?, \'U=1\', ?);",
(
Chattype::Broadcast,
&chat_name,
&grpid,
create_smeared_timestamp(context),
),
)?;
Ok(t.last_insert_rowid().try_into()?)
};
context.sql.transaction(trans_fn).await?
};
let chat_id = ChatId::new(u32::try_from(row_id)?);
context.emit_msgs_changed_without_ids();
if sync.into() {
let id = SyncId::Grpid(grpid);
let action = SyncAction::CreateBroadcast(chat_name);
self::sync(context, id, action).await.log_err(context).ok();
}
Ok(chat_id)
}
/// Set chat contacts in the `chats_contacts` table.
pub(crate) async fn update_chat_contacts_table(
context: &Context,
id: ChatId,
contacts: &HashSet<ContactId>,
) -> Result<()> {
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
for contact_id in contacts {
transaction.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
(id, contact_id),
)?;
}
Ok(())
})
.await?;
Ok(())
}
/// Adds contacts to the `chats_contacts` table.
pub(crate) async fn add_to_chat_contacts_table(
context: &Context,
@@ -3258,12 +3375,13 @@ pub async fn add_contact_to_chat(
chat_id: ChatId,
contact_id: ContactId,
) -> Result<()> {
add_contact_to_chat_ex(context, chat_id, contact_id, false).await?;
add_contact_to_chat_ex(context, Sync, chat_id, contact_id, false).await?;
Ok(())
}
pub(crate) async fn add_contact_to_chat_ex(
context: &Context,
mut sync: sync::Sync,
chat_id: ChatId,
contact_id: ContactId,
from_handshake: bool,
@@ -3302,8 +3420,12 @@ pub(crate) async fn add_contact_to_chat_ex(
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
chat.param.remove(Param::Unpromoted);
chat.update_param(context).await?;
context.sync_qr_code_tokens(Some(chat_id)).await?;
context.send_sync_msg().await?;
let _ = context
.sync_qr_code_tokens(Some(chat_id))
.await
.log_err(context)
.is_ok()
&& context.send_sync_msg().await.log_err(context).is_ok();
}
if context.is_self_addr(contact.get_addr()).await? {
@@ -3339,14 +3461,18 @@ pub(crate) async fn add_contact_to_chat_ex(
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
let contact_addr = contact.get_addr();
msg.text = stock_str::msg_add_member_local(context, contact_addr, ContactId::SELF).await;
let contact_addr = contact.get_addr().to_lowercase();
msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
msg.id = send_msg(context, chat_id, &mut msg).await?;
sync = Nosync;
}
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
}
Ok(true)
}
@@ -3453,8 +3579,10 @@ pub(crate) async fn set_muted_ex(
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?;
chat.sync(context, SyncAction::SetMuted(duration))
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -3486,6 +3614,7 @@ pub async fn remove_contact_from_chat(
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
bail!("{}", err_msg);
} else {
let mut sync = Nosync;
// We do not return an error if the contact does not exist in the database.
// This allows to delete dangling references to deleted contacts
// in case of the database becoming inconsistent due to a bug.
@@ -3504,8 +3633,10 @@ pub async fn remove_contact_from_chat(
.await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr());
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
msg.id = send_msg(context, chat_id, &mut msg).await?;
} else {
sync = Sync;
}
}
// we remove the member from the chat after constructing the
@@ -3520,6 +3651,9 @@ pub async fn remove_contact_from_chat(
// check/encryption logic.
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
}
}
} else {
bail!("Cannot remove members from non-group chats.");
@@ -3549,6 +3683,15 @@ pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) ->
/// Sets group or mailing list chat name.
pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
rename_ex(context, Sync, chat_id, new_name).await
}
async fn rename_ex(
context: &Context,
sync: sync::Sync,
chat_id: ChatId,
new_name: &str,
) -> Result<()> {
let new_name = improve_single_line_input(new_name);
/* the function only sets the names of group chats; normal chats get their names from the contacts */
let mut success = false;
@@ -3600,7 +3743,13 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
if !success {
bail!("Failed to set name");
}
if sync.into() && chat.name != new_name {
let sync_name = new_name.to_string();
chat.sync(context, SyncAction::Rename(sync_name))
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -4074,15 +4223,59 @@ pub(crate) async fn update_msg_text_and_timestamp(
Ok(())
}
/// Set chat contacts by their addresses creating the corresponding contacts if necessary.
async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> {
let chat = Chat::load_from_db(context, id).await?;
ensure!(
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
"{id} is not a group/broadcast",
);
let mut contacts = HashSet::new();
for addr in addrs {
let contact_addr = ContactAddress::new(addr)?;
let contact = Contact::add_or_lookup(context, "", &contact_addr, Origin::Hidden)
.await?
.0;
contacts.insert(contact);
}
let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
if contacts == contacts_old {
return Ok(());
}
update_chat_contacts_table(context, id, &contacts).await?;
context.emit_event(EventType::ChatModified(id));
Ok(())
}
/// A cross-device chat id used for synchronisation.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum SyncId {
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.
}
/// An action synchronised to other devices.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum SyncAction {
Block,
Unblock,
Accept,
SetVisibility(ChatVisibility),
SetMuted(MuteDuration),
/// Create broadcast list with the given name.
CreateBroadcast(String),
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec<String>),
}
impl Context {
/// Executes [`SyncData::AlterChat`] item sent by other device.
pub(crate) async fn sync_alter_chat(
&self,
id: &sync::ChatId,
action: &ChatAction,
) -> Result<()> {
pub(crate) async fn sync_alter_chat(&self, id: &SyncId, action: &SyncAction) -> Result<()> {
let chat_id = match id {
sync::ChatId::ContactAddr(addr) => {
SyncId::ContactAddr(addr) => {
let Some(contact_id) =
Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None).await?
else {
@@ -4090,10 +4283,10 @@ impl Context {
return Ok(());
};
match action {
ChatAction::Block => {
SyncAction::Block => {
return contact::set_blocked(self, Nosync, contact_id, true).await
}
ChatAction::Unblock => {
SyncAction::Unblock => {
return contact::set_blocked(self, Nosync, contact_id, false).await
}
_ => (),
@@ -4104,7 +4297,11 @@ impl Context {
};
chat_id
}
sync::ChatId::Grpid(grpid) => {
SyncId::Grpid(grpid) => {
if let SyncAction::CreateBroadcast(name) = action {
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
return Ok(());
}
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(());
@@ -4113,14 +4310,17 @@ impl Context {
}
};
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,
SyncAction::Block => chat_id.block_ex(self, Nosync).await,
SyncAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
SyncAction::Accept => chat_id.accept_ex(self, Nosync).await,
SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
SyncAction::CreateBroadcast(_) => {
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
}
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
}
.ok();
Ok(())
}
}
@@ -4133,6 +4333,7 @@ mod tests {
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use strum::IntoEnumIterator;
use tokio::fs;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -4343,7 +4544,7 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let added = add_contact_to_chat_ex(&t, chat_id, ContactId::SELF, false)
let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false)
.await
.unwrap();
assert_eq!(added, false);
@@ -4485,6 +4686,37 @@ mod tests {
Ok(())
}
/// Test that if a message implicitly adds a member, both messages appear.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_msg_with_implicit_member_add() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_bob_contact_id =
Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?;
let fiona_addr = "fiona@example.net";
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?;
let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?;
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent_msg = alice.send_text(alice_chat_id, "I created a group").await;
let bob_received_msg = bob.recv_msg(&sent_msg).await;
let bob_chat_id = bob_received_msg.get_chat_id();
bob_chat_id.accept(&bob).await?;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
let sent_msg = alice.pop_sent_msg().await;
bob.recv_msg(&sent_msg).await;
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
bob.recv_msg(&sent_msg).await;
bob.golden_test_chat(bob_chat_id, "chat_test_msg_with_implicit_member_add")
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_chat_multi_device() -> Result<()> {
let a1 = TestContext::new_alice().await;
@@ -4693,7 +4925,7 @@ mod tests {
// adding or removing contacts from one-to-one-chats result in an error
let claire = Contact::create(&ctx, "", "claire@foo.de").await.unwrap();
let added = add_contact_to_chat_ex(&ctx, chat.id, claire, false).await;
let added = add_contact_to_chat_ex(&ctx, Nosync, chat.id, claire, false).await;
assert!(added.is_err());
assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1);
@@ -5359,7 +5591,7 @@ mod tests {
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("foo@bar.org")?,
&ContactAddress::new("foo@bar.org")?,
Origin::IncomingUnknownTo,
)
.await?;
@@ -6323,13 +6555,54 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_broadcast_multidev() -> Result<()> {
let alices = [
TestContext::new_alice().await,
TestContext::new_alice().await,
];
let bob = TestContext::new_bob().await;
let a1b_contact_id = alices[1].add_or_lookup_contact(&bob).await.id;
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
let sent_msg = alices[0].send_text(a0_broadcast_id, "hi").await;
let msg = alices[1].recv_msg(&sent_msg).await;
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
.await?
.unwrap()
.0;
assert_eq!(msg.chat_id, a1_broadcast_id);
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
.await?
.is_empty());
add_contact_to_chat(&alices[1], a1_broadcast_id, a1b_contact_id).await?;
set_chat_name(&alices[1], a1_broadcast_id, "Broadcast list 43").await?;
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
let msg = alices[0].recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, a0_broadcast_id);
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
assert_eq!(a0_broadcast_chat.get_type(), Chattype::Broadcast);
assert_eq!(a0_broadcast_chat.get_name(), "Broadcast list 42");
assert!(get_chat_contacts(&alices[0], a0_broadcast_id)
.await?
.is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("foo@bar.org")?,
&ContactAddress::new("foo@bar.org")?,
Origin::ManuallyCreated,
)
.await?;
@@ -6611,4 +6884,159 @@ mod tests {
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_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;
let ab_contact_ids = [
alices[0].add_or_lookup_contact(&bob).await.id,
alices[1].add_or_lookup_contact(&bob).await.id,
];
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.
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::block(&alices[0], ab_contact_ids[0]).await?;
sync(&alices).await?;
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::unblock(&alices[0], ab_contact_ids[0]).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);
}
}
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 {
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);
}
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
sync(&alices).await?;
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
sync(&alices).await?;
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
.await?
.unwrap()
.0;
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
.await?
.is_empty());
add_contact_to_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
sync(&alices).await?;
assert_eq!(
get_chat_contacts(&alices[1], a1_broadcast_id).await?,
vec![ab_contact_ids[1]]
);
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
let msg = bob.recv_msg(&sent_msg).await;
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.get_type(), Chattype::Mailinglist);
let msg = alices[0].recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, a0_broadcast_id);
remove_contact_from_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
sync(&alices).await?;
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
.await?
.is_empty());
Ok(())
}
}

View File

@@ -513,6 +513,11 @@ impl Context {
);
self.sql.set_raw_config(key.as_ref(), value).await?;
}
Config::Addr => {
self.sql
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
.await?;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -662,6 +667,21 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_addr() {
let t = TestContext::new().await;
// Test that uppercase address get lowercased.
assert!(t
.set_config(Config::Addr, Some("Foobar@eXample.oRg"))
.await
.is_ok());
assert_eq!(
t.get_config(Config::Addr).await.unwrap().unwrap(),
"foobar@example.org"
);
}
/// Tests that "bot" config can only be set to "0" or "1".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bot() {

View File

@@ -43,43 +43,43 @@ use crate::{chat, stock_str};
const SEEN_RECENTLY_SECONDS: i64 = 600;
/// Valid contact address.
#[derive(Debug, Clone, Copy)]
pub(crate) struct ContactAddress<'a>(&'a str);
#[derive(Debug, Clone)]
pub(crate) struct ContactAddress(String);
impl Deref for ContactAddress<'_> {
impl Deref for ContactAddress {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
&self.0
}
}
impl AsRef<str> for ContactAddress<'_> {
impl AsRef<str> for ContactAddress {
fn as_ref(&self) -> &str {
self.0
&self.0
}
}
impl fmt::Display for ContactAddress<'_> {
impl fmt::Display for ContactAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'a> ContactAddress<'a> {
impl ContactAddress {
/// Constructs a new contact address from string,
/// normalizing and validating it.
pub fn new(s: &'a str) -> Result<Self> {
pub fn new(s: &str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(addr) {
if !may_be_valid_addr(&addr) {
bail!("invalid address {:?}", s);
}
Ok(Self(addr))
Ok(Self(addr.to_string()))
}
}
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress<'_> {
impl rusqlite::types::ToSql for ContactAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.0.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
@@ -149,6 +149,22 @@ impl ContactId {
.await?;
Ok(())
}
/// Reset gossip timestamp in all chats with this contact.
pub(crate) async fn regossip_keys(&self, context: &Context) -> Result<()> {
context
.sql
.execute(
"UPDATE chats
SET gossiped_timestamp=0
WHERE EXISTS (SELECT 1 FROM chats_contacts
WHERE chats_contacts.chat_id=chats.id
AND chats_contacts.contact_id=?)",
(self,),
)
.await?;
Ok(())
}
}
impl fmt::Display for ContactId {
@@ -484,7 +500,7 @@ impl Contact {
let addr = ContactAddress::new(&addr)?;
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, &name, addr, Origin::ManuallyCreated)
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated)
.await
.context("add_or_lookup")?;
let blocked = Contact::is_blocked_load(context, contact_id).await?;
@@ -549,7 +565,7 @@ impl Contact {
let addr_normalized = addr_normalize(addr);
if context.is_self_addr(addr_normalized).await? {
if context.is_self_addr(&addr_normalized).await? {
return Ok(Some(ContactId::SELF));
}
@@ -599,7 +615,7 @@ impl Contact {
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
addr: ContactAddress<'_>,
addr: &ContactAddress,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
@@ -607,7 +623,7 @@ impl Contact {
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
ensure!(origin != Origin::Unknown, "Missing valid origin");
if context.is_self_addr(&addr).await? {
if context.is_self_addr(addr).await? {
return Ok((ContactId::SELF, sth_modified));
}
@@ -762,7 +778,7 @@ impl Contact {
} else {
"".to_string()
},
addr,
&addr,
origin,
if update_authname {
name.to_string()
@@ -807,7 +823,7 @@ impl Contact {
let name = normalize_name(&name);
match ContactAddress::new(&addr) {
Ok(addr) => {
match Contact::add_or_lookup(context, &name, addr, Origin::AddressBook).await {
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
Ok((_, modified)) => {
if modified != Modifier::None {
modify_cnt += 1
@@ -1301,7 +1317,7 @@ impl Contact {
return Ok(None);
};
if verifier_addr == self.addr {
if addr_cmp(&verifier_addr, &self.addr) {
// Contact is directly verified via QR code.
return Ok(Some(ContactId::SELF));
}
@@ -1389,12 +1405,13 @@ pub fn may_be_valid_addr(addr: &str) -> bool {
res.is_ok()
}
/// Returns address with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> &str {
let norm = addr.trim();
/// Returns address lowercased,
/// with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> String {
let norm = addr.trim().to_lowercase();
if norm.starts_with("mailto:") {
norm.get(7..).unwrap_or(norm)
norm.get(7..).unwrap_or(&norm).to_string()
} else {
norm
}
@@ -1480,12 +1497,12 @@ WHERE type=? AND id IN (
if sync.into() {
let action = match new_blocking {
true => sync::ChatAction::Block,
false => sync::ChatAction::Unblock,
true => chat::SyncAction::Block,
false => chat::SyncAction::Unblock,
};
context
.add_sync_item(SyncData::AlterChat {
id: sync::ChatId::ContactAddr(contact.addr.clone()),
id: chat::SyncId::ContactAddr(contact.addr.clone()),
action,
})
.await?;
@@ -1649,8 +1666,8 @@ fn cat_fingerprint(
/// Compares two email addresses, normalizing them beforehand.
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
let norm1 = addr_normalize(addr1).to_lowercase();
let norm2 = addr_normalize(addr2).to_lowercase();
let norm1 = addr_normalize(addr1);
let norm2 = addr_normalize(addr2);
norm1 == norm2
}
@@ -1848,10 +1865,7 @@ mod tests {
fn test_normalize_addr() {
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com");
// normalisation preserves case to allow user-defined spelling.
// however, case is ignored on addr_cmp()
assert_ne!(addr_normalize("John@Doe.com"), "john@doe.com");
assert_eq!(addr_normalize("John@Doe.com"), "john@doe.com");
}
#[test]
@@ -1877,7 +1891,7 @@ mod tests {
let (id, _modified) = Contact::add_or_lookup(
&context.ctx,
"bob",
ContactAddress::new("user@example.org")?,
&ContactAddress::new("user@example.org")?,
Origin::IncomingReplyTo,
)
.await?;
@@ -1905,7 +1919,7 @@ mod tests {
let (contact_bob_id, modified) = Contact::add_or_lookup(
&context.ctx,
"someone",
ContactAddress::new("user@example.org")?,
&ContactAddress::new("user@example.org")?,
Origin::ManuallyCreated,
)
.await?;
@@ -1970,7 +1984,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bla foo",
ContactAddress::new("one@eins.org").unwrap(),
&ContactAddress::new("one@eins.org").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -1989,7 +2003,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"Real one",
ContactAddress::new(" one@eins.org ").unwrap(),
&ContactAddress::new(" one@eins.org ").unwrap(),
Origin::ManuallyCreated,
)
.await
@@ -2005,7 +2019,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("three@drei.sam").unwrap(),
&ContactAddress::new("three@drei.sam").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -2022,7 +2036,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"m. serious",
ContactAddress::new("three@drei.sam").unwrap(),
&ContactAddress::new("three@drei.sam").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2037,7 +2051,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"schnucki",
ContactAddress::new("three@drei.sam").unwrap(),
&ContactAddress::new("three@drei.sam").unwrap(),
Origin::ManuallyCreated,
)
.await
@@ -2053,7 +2067,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("alice@w.de").unwrap(),
&ContactAddress::new("alice@w.de").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -2195,7 +2209,7 @@ mod tests {
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -2274,7 +2288,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob1",
ContactAddress::new("bob@example.org").unwrap(),
&ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2290,7 +2304,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob2",
ContactAddress::new("bob@example.org").unwrap(),
&ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2316,7 +2330,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob4",
ContactAddress::new("bob@example.org").unwrap(),
&ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2345,7 +2359,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"claire1",
ContactAddress::new("claire@example.org").unwrap(),
&ContactAddress::new("claire@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2361,7 +2375,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"claire2",
ContactAddress::new("claire@example.org").unwrap(),
&ContactAddress::new("claire@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2386,7 +2400,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"Bob",
ContactAddress::new("bob@example.org")?,
&ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownFrom,
)
.await?;
@@ -2398,7 +2412,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"Not Bob",
ContactAddress::new("bob@example.org")?,
&ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownTo,
)
.await?;
@@ -2411,7 +2425,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"Bob",
ContactAddress::new("bob@example.org")?,
&ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownFrom,
)
.await?;
@@ -2440,7 +2454,7 @@ mod tests {
Contact::add_or_lookup(
&t,
"dave2",
ContactAddress::new("dave@example.org").unwrap(),
&ContactAddress::new("dave@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2561,7 +2575,7 @@ mod tests {
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -2724,7 +2738,7 @@ CCCB 5AA9 F6E1 141C 9431
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;

View File

@@ -413,7 +413,8 @@ impl Context {
.is_some()
{
let mut lock = self.ratelimit.write().await;
*lock = Ratelimit::new(Duration::new(40, 0), 6.0);
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
}
self.scheduler.start(self.clone()).await;

View File

@@ -619,11 +619,11 @@ impl Imap {
.inner
.status(folder, "(UIDNEXT)")
.await
.context("STATUS (UIDNEXT) error for {folder:?}")?;
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
status
.uid_next
.context("STATUS {folder} (UIDNEXT) did not return UIDNEXT")?
.with_context(|| format!("STATUS {folder} (UIDNEXT) did not return UIDNEXT"))?
};
mailbox.uid_next = Some(new_uid_next);
@@ -2519,7 +2519,7 @@ async fn add_all_recipients_as_contacts(
let (_, modified) = Contact::add_or_lookup(
context,
&display_name_normalized,
recipient_addr,
&recipient_addr,
Origin::OutgoingTo,
)
.await?;

View File

@@ -35,7 +35,7 @@ impl Session {
let status = self
.status(folder, "(UIDNEXT)")
.await
.context("STATUS (UIDNEXT) error for {folder:?}")?;
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
if let Some(uid_next) = status.uid_next {
let expected_uid_next = get_uid_next(context, folder)
.await

View File

@@ -316,7 +316,15 @@ impl<'a> MimeFactory<'a> {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
PeerstateVerifiedStatus::BidirectVerified
if self.msg.get_info_type() == SystemMessage::SecurejoinMessage {
// Securejoin messages are supposed to verify a key.
// In order to do this, it is necessary that they can be sent
// to a key that is not yet verified.
// This has to work independently of whether the chat is protected right now.
PeerstateVerifiedStatus::Unverified
} else {
PeerstateVerifiedStatus::BidirectVerified
}
} else {
PeerstateVerifiedStatus::Unverified
}
@@ -596,9 +604,10 @@ impl<'a> MimeFactory<'a> {
if let Loaded::Message { chat } = &self.loaded {
if chat.typ == Chattype::Broadcast {
let encoded_chat_name = encode_words(&chat.name);
headers.protected.push(Header::new(
"List-ID".into(),
format!("{} <{}>", chat.name, chat.grpid),
format!("{encoded_chat_name} <{}>", chat.grpid),
));
}
}
@@ -1910,7 +1919,7 @@ mod tests {
let contact_id = Contact::add_or_lookup(
&t,
"Dave",
ContactAddress::new("dave@example.com").unwrap(),
&ContactAddress::new("dave@example.com").unwrap(),
Origin::ManuallyCreated,
)
.await

View File

@@ -163,10 +163,10 @@ pub enum SystemMessage {
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10,
/// Chat protection is enabled.
/// "Messages are guaranteed to be end-to-end encrypted from now on."
ChatProtectionEnabled = 11,
/// Chat protection is disabled.
/// "%1$s sent a message from another device."
ChatProtectionDisabled = 12,
/// Self-sent-message that contains only json used for multi-device-sync;

View File

@@ -648,7 +648,7 @@ impl Peerstate {
let (new_contact_id, _) = Contact::add_or_lookup(
context,
"",
new_addr,
&new_addr,
Origin::IncomingUnknownFrom,
)
.await?;

View File

@@ -365,9 +365,10 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan)
.await
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan)
.await
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
if let (Some(grpid), Some(grpname)) = (grpid, grpname) {
if context
@@ -432,7 +433,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
if let Some(peerstate) = peerstate {
let peerstate_addr = ContactAddress::new(&peerstate.addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan)
Contact::add_or_lookup(context, &name, &peerstate_addr, Origin::UnhandledQrScan)
.await
.context("add_or_lookup")?;
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
@@ -777,7 +778,7 @@ impl Qr {
) -> Result<Self> {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await?;
Contact::add_or_lookup(context, name, &addr, Origin::UnhandledQrScan).await?;
Ok(Qr::Addr { contact_id, draft })
}
}
@@ -788,7 +789,7 @@ fn normalize_address(addr: &str) -> Result<String> {
let new_addr = percent_decode_str(addr).decode_utf8()?;
let new_addr = addr_normalize(&new_addr);
ensure!(may_be_valid_addr(new_addr), "Bad e-mail address");
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
Ok(new_addr.to_string())
}

View File

@@ -392,7 +392,7 @@ Can we chat at 1pm pacific, today?"
let bob_id = Contact::add_or_lookup(
&alice,
"",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?

View File

@@ -14,7 +14,7 @@ use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
addr_cmp, may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -227,8 +227,7 @@ pub(crate) async fn receive_imf_inner(
.and_then(|value| mailparse::dateparse(value).ok())
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp + 60));
let updated_verified_key_addr =
update_verified_keys(context, &mut mime_parser, from_id).await?;
update_verified_keys(context, &mut mime_parser, from_id).await?;
// Add parts
let received_msg = add_parts(
@@ -281,19 +280,12 @@ pub(crate) async fn receive_imf_inner(
MsgId::new_unset()
};
if let Some(addr) = updated_verified_key_addr {
let msg = stock_str::contact_setup_changed(context, &addr).await;
chat::add_info_msg(context, chat_id, &msg, received_msg.sort_timestamp).await?;
}
save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
if let Some(ref sync_items) = mime_parser.sync_items {
if from_id == ContactId::SELF {
if mime_parser.was_encrypted() {
if let Err(err) = context.execute_sync_items(sync_items).await {
warn!(context, "receive_imf cannot execute sync items: {err:#}.");
}
context.execute_sync_items(sync_items).await;
} else {
warn!(context, "Sync items are not encrypted.");
}
@@ -465,7 +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();
let mut group_changes_msgs = (Vec::new(), None);
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
}
@@ -562,6 +554,11 @@ async fn add_parts(
markseen_on_imap_table(context, rfc724_mid).await.ok();
}
if chat_id.is_none() && is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is an MDN (TRASH).",);
}
if chat_id.is_none() {
// try to assign to a chat based on In-Reply-To/References:
@@ -639,14 +636,9 @@ async fn add_parts(
if let Some(group_chat_id) = chat_id {
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
let chat = Chat::load_from_db(context, group_chat_id).await?;
if chat.is_protected() {
if chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
} else {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.replace_msg_by_error(&s);
}
if chat.is_protected() && chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
} else {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
@@ -654,6 +646,12 @@ async fn add_parts(
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
if chat.is_protected() {
// In protected chat, also mark the message with an error.
let s = stock_str::unknown_sender_for_chat(context).await;
part.error = Some(s);
}
}
}
}
@@ -932,6 +930,22 @@ async fn add_parts(
}
}
}
if chat_id.is_none() {
// Check if the message belongs to a broadcast list.
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
let listid = mailinglist_header_listid(mailinglist_header)?;
chat_id = Some(
if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
id
} else {
let name =
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
},
);
}
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
@@ -1124,9 +1138,29 @@ async fn add_parts(
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
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 let Some(msg) = group_changes_msgs.1 {
match &better_msg {
None => better_msg = Some(msg),
Some(_) => group_changes_msgs.0.push(msg),
}
}
for group_changes_msg in group_changes_msgs.0 {
// Currently all additional group changes messages are "Member added".
chat::add_info_msg_with_cmd(
context,
chat_id,
&group_changes_msg,
SystemMessage::MemberAddedToGroup,
sort_timestamp,
None,
None,
None,
)
.await?;
}
for part in &mime_parser.parts {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
set_msg_reaction(
@@ -1159,10 +1193,7 @@ async fn add_parts(
}
let mut txt_raw = "".to_string();
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 {
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
(better_msg, Viewtype::Text)
} else {
(&part.msg, part.typ)
@@ -1290,10 +1321,6 @@ 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
@@ -1678,17 +1705,6 @@ async fn create_or_lookup_group(
members.dedup();
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
// once, we have protected-chats explained in UI, we can uncomment the following lines.
// ("verified groups" did not add a message anyway)
//
//if create_protected == ProtectionStatus::Protected {
// set from_id=0 as it is not clear that the sender of this random group message
// actually really has enabled chat-protection at some point.
//new_chat_id
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
// .await?;
//}
context.emit_event(EventType::ChatModified(new_chat_id));
}
@@ -1722,24 +1738,25 @@ async fn apply_group_changes(
from_id: ContactId,
to_ids: &[ContactId],
is_partial_download: bool,
) -> Result<Vec<String>> {
) -> Result<(Vec<String>, Option<String>)> {
if chat_id.is_special() {
// Do not apply group changes to the trash chat.
return Ok(Vec::new());
return Ok((Vec::new(), None));
}
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Group {
return Ok(Vec::new());
return Ok((Vec::new(), None));
}
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 =
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
context.get_primary_self_addr().await? == *added_addr
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
} else {
false
};
@@ -1791,7 +1808,12 @@ async fn apply_group_changes(
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.set_protection(
context,
ProtectionStatus::Protected,
smeared_time(context),
Some(from_id),
)
.await?;
}
}
@@ -1799,11 +1821,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?;
group_changes_msgs.push(if removed_id == Some(from_id) {
stock_str::msg_group_left_local(context, from_id).await
better_msg = if removed_id == Some(from_id) {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
stock_str::msg_del_member_local(context, removed_addr, from_id).await
});
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
if removed_id.is_some() {
if !allow_member_list_changes {
@@ -1816,8 +1838,7 @@ 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) {
group_changes_msgs
.push(stock_str::msg_add_member_local(context, added_addr, from_id).await);
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if allow_member_list_changes {
if !recreate_member_list {
@@ -1858,20 +1879,21 @@ async fn apply_group_changes(
send_event_chat_modified = true;
}
group_changes_msgs
.push(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
better_msg = Some(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
group_changes_msgs.push(match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
better_msg = match avatar_action {
AvatarAction::Delete => {
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
}
});
AvatarAction::Change(_) => {
Some(stock_str::msg_grp_img_changed(context, from_id).await)
}
};
}
}
}
@@ -1929,21 +1951,7 @@ async fn apply_group_changes(
}
if new_members != chat_contacts {
let new_members_ref = &new_members;
context
.sql
.transaction(move |transaction| {
transaction
.execute("DELETE FROM chats_contacts WHERE chat_id=?", (chat_id,))?;
for contact_id in new_members_ref {
transaction.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
(chat_id, contact_id),
)?;
}
Ok(())
})
.await?;
chat::update_chat_contacts_table(context, chat_id, &new_members).await?;
chat_contacts = new_members;
send_event_chat_modified = true;
}
@@ -1983,11 +1991,22 @@ async fn apply_group_changes(
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat_id));
}
Ok(group_changes_msgs)
Ok((group_changes_msgs, better_msg))
}
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
Ok(match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
None => list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>'),
}
.to_string())
}
/// Create or lookup a mailing list chat.
///
/// `list_id_header` contains the Id that must be used for the mailing list
@@ -1997,21 +2016,13 @@ static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unw
///
/// `mime_parser` is the corresponding message
/// and is used to figure out the mailing list name from different header fields.
#[allow(clippy::indexing_slicing)]
async fn create_or_lookup_mailinglist(
context: &Context,
allow_creation: bool,
list_id_header: &str,
mime_parser: &MimeMessage,
) -> Result<Option<(ChatId, Blocked)>> {
let listid = match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap[2].trim().to_string(),
None => list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.to_string(),
};
let listid = mailinglist_header_listid(list_id_header)?;
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
return Ok(Some((chat_id, blocked)));
@@ -2166,7 +2177,7 @@ async fn apply_mailinglist_changes(
return Ok(());
}
};
let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, listid);
@@ -2286,9 +2297,6 @@ enum VerifiedEncryption {
/// Moves secondary verified key to primary verified key
/// if the message is signed with a secondary verified key.
/// Removes secondary verified key if the message is signed with primary key.
///
/// Returns address of the peerstate if the primary verified key was updated,
/// the caller then needs to add "Setup changed" notification somewhere.
async fn update_verified_keys(
context: &Context,
mimeparser: &mut MimeMessage,
@@ -2336,10 +2344,11 @@ async fn update_verified_keys(
peerstate.verified_key = peerstate.secondary_verified_key.take();
peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take();
peerstate.verifier = peerstate.secondary_verifier.take();
peerstate.fingerprint_changed = true;
peerstate.save_to_db(&context.sql).await?;
// Primary verified key changed.
Ok(Some(peerstate.addr.clone()))
Ok(None)
} else {
Ok(None)
}
@@ -2411,6 +2420,16 @@ async fn has_verified_encryption(
return Ok(Verified);
}
mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?;
Ok(Verified)
}
async fn mark_recipients_as_verified(
context: &Context,
from_id: ContactId,
to_ids: Vec<ContactId>,
mimeparser: &MimeMessage,
) -> Result<(), anyhow::Error> {
let rows = context
.sql
.query_map(
@@ -2453,6 +2472,17 @@ async fn has_verified_encryption(
if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
peerstate.set_verified(PeerstateKeyType::GossipKey, fp, verifier_addr)?;
peerstate.save_to_db(&context.sql).await?;
if !is_verified {
let (to_contact_id, _) = Contact::add_or_lookup(
context,
"",
&ContactAddress::new(&to_addr)?,
Origin::Hidden,
)
.await?;
ChatId::set_protection_for_contact(context, to_contact_id).await?;
}
}
} else {
// The contact already has a verified key.
@@ -2463,7 +2493,8 @@ async fn has_verified_encryption(
}
}
}
Ok(Verified)
Ok(())
}
/// Returns the last message referenced from `References` header if it is in the database.
@@ -2584,7 +2615,7 @@ async fn add_or_lookup_contacts_by_address_list(
async fn add_or_lookup_contact_by_addr(
context: &Context,
display_name: Option<&str>,
addr: ContactAddress<'_>,
addr: ContactAddress,
origin: Origin,
) -> Result<ContactId> {
if context.is_self_addr(&addr).await? {
@@ -2593,7 +2624,7 @@ async fn add_or_lookup_contact_by_addr(
let display_name_normalized = display_name.map(normalize_name).unwrap_or_default();
let (contact_id, _modified) =
Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?;
Contact::add_or_lookup(context, &display_name_normalized, &addr, origin).await?;
Ok(contact_id)
}

View File

@@ -431,7 +431,7 @@ async fn test_escaped_recipients() {
let carl_contact_id = Contact::add_or_lookup(
&t,
"Carl",
ContactAddress::new("carl@host.tld").unwrap(),
&ContactAddress::new("carl@host.tld").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -477,7 +477,7 @@ async fn test_cc_to_contact() {
let carl_contact_id = Contact::add_or_lookup(
&t,
"garabage",
ContactAddress::new("carl@host.tld").unwrap(),
&ContactAddress::new("carl@host.tld").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2003,7 +2003,7 @@ async fn test_duplicate_message() -> Result<()> {
let bob_contact_id = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.org").unwrap(),
&ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await?
@@ -2060,7 +2060,7 @@ async fn test_ignore_footer_status_from_mailinglist() -> Result<()> {
let bob_id = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("bob@example.net").unwrap(),
&ContactAddress::new("bob@example.net").unwrap(),
Origin::IncomingUnknownCc,
)
.await?
@@ -2139,7 +2139,7 @@ async fn test_ignore_old_status_updates() -> Result<()> {
let bob_id = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::AddressBook,
)
.await?
@@ -2623,19 +2623,17 @@ async fn test_incoming_contact_request() -> Result<()> {
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(chat.is_contact_request());
loop {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
match event {
EventType::IncomingMsg { chat_id, msg_id } => {
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.id, msg_id);
return Ok(());
}
_ => unreachable!(),
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
match event {
EventType::IncomingMsg { chat_id, msg_id } => {
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.id, msg_id);
Ok(())
}
_ => unreachable!(),
}
}

View File

@@ -2,7 +2,7 @@ use core::fmt;
use std::cmp::min;
use std::{iter::once, ops::Deref, sync::Arc};
use anyhow::{anyhow, Result};
use anyhow::Result;
use humansize::{format_size, BINARY};
use tokio::sync::Mutex;
@@ -299,6 +299,10 @@ impl Context {
.yellow {
background-color: #fdc625;
}
.not-started-error {
font-size: 2em;
color: red;
}
</style>
</head>
<body>"#
@@ -318,7 +322,8 @@ impl Context {
sched.smtp.state.connectivity.clone(),
),
_ => {
return Err(anyhow!("Not started"));
ret += "<div class=\"not-started-error\">Error: IO Not Started</div><p>Please report this issue to the app developer.</p>\n</body></html>\n";
return Ok(ret);
}
};
drop(lock);

View File

@@ -1,4 +1,4 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
//! Verified contact protocol implementation as [specified by countermitm project](https://securejoin.readthedocs.io/en/latest/new.html#setup-contact-protocol).
use std::convert::TryFrom;
@@ -21,6 +21,7 @@ use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateKeyType};
use crate::qr::check_qr;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::token;
use crate::tools::time;
@@ -425,6 +426,7 @@ pub(crate) async fn handle_securejoin_handshake(
.await?;
return Ok(HandshakeMessage::Ignore);
}
contact_id.regossip_keys(context).await?;
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
info!(context, "Auth verified.",);
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
@@ -443,8 +445,14 @@ 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?;
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await?;
chat::add_contact_to_chat_ex(
context,
Nosync,
group_chat_id,
contact_id,
true,
)
.await?;
}
None => bail!("Chat {} not found", &field_grpid),
}
@@ -609,6 +617,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
peerstate.set_verified(PeerstateKeyType::GossipKey, fingerprint, addr)?;
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
ChatId::set_protection_for_contact(context, contact_id).await?;
} else if let Some(fingerprint) =
mime_message.get_header(HeaderDef::SecureJoinFingerprint)
{
@@ -675,9 +685,6 @@ async fn secure_connection_established(
contact_id: ContactId,
chat_id: ChatId,
) -> Result<()> {
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_verified(context, &contact).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
if context
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
@@ -763,7 +770,7 @@ fn encrypted_and_signed(
mod tests {
use super::*;
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::chat::{remove_contact_from_chat, ProtectionStatus};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactAddress;
@@ -915,25 +922,10 @@ mod tests {
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
let msg_ids: Vec<_> = chat::get_chat_msgs(&alice.ctx, chat.get_id())
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.collect();
assert_eq!(msg_ids.len(), 2);
let msg0 = Message::load_from_db(&alice.ctx, msg_ids[0]).await.unwrap();
assert!(msg0.is_info());
assert!(msg0.get_text().contains("bob@example.net verified"));
let msg1 = Message::load_from_db(&alice.ctx, msg_ids[1]).await.unwrap();
assert!(msg1.is_info());
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
assert!(msg.is_info());
let expected_text = chat_protection_enabled(&alice).await;
assert_eq!(msg1.get_text(), expected_text);
assert_eq!(msg.get_text(), expected_text);
}
// Check Alice sent the right message to Bob.
@@ -969,24 +961,10 @@ mod tests {
// Check Bob got the verified message in his 1:1 chat.
{
let chat = bob.create_chat(&alice).await;
let msg_ids: Vec<_> = chat::get_chat_msgs(&bob.ctx, chat.get_id())
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.collect();
let msg0 = Message::load_from_db(&bob.ctx, msg_ids[0]).await.unwrap();
assert!(msg0.is_info());
assert!(msg0.get_text().contains("alice@example.org verified"));
let msg1 = Message::load_from_db(&bob.ctx, msg_ids[1]).await.unwrap();
assert!(msg1.is_info());
let msg = get_chat_msg(&bob, chat.get_id(), 0, 1).await;
assert!(msg.is_info());
let expected_text = chat_protection_enabled(&bob).await;
assert_eq!(msg1.get_text(), expected_text);
assert_eq!(msg.get_text(), expected_text);
}
// Check Bob sent the final message
@@ -1080,7 +1058,7 @@ mod tests {
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice.ctx,
"Bob",
ContactAddress::new("bob@example.net")?,
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -1285,11 +1263,11 @@ mod tests {
);
// There should be 3 messages in the chat:
// - The ChatProtectionEnabled message
// - bob@example.net verified
// - You added member bob@example.net
let msg = get_chat_msg(&alice, alice_chatid, 1, 3).await;
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
assert!(msg.is_info());
assert!(msg.get_text().contains("bob@example.net verified"));
let expected_text = chat_protection_enabled(&alice).await;
assert_eq!(msg.get_text(), expected_text);
}
// Bob should not yet have Alice verified
@@ -1325,27 +1303,6 @@ mod tests {
println!("msg {msg_id} text: {text}");
}
}
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid)
.await
.unwrap()
.into_iter();
loop {
match msg_iter.next() {
Some(chat::ChatItem::Message { msg_id }) => {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text();
match text.contains("alice@example.org verified") {
true => {
assert!(msg.is_info());
break;
}
false => continue,
}
}
Some(_) => continue,
None => panic!("Verified message not found in Bob's group chat"),
}
}
}
let sent = bob.pop_sent_msg().await;
@@ -1395,4 +1352,32 @@ First thread."#;
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unknown_sender() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
tcm.execute_securejoin(&alice, &bob).await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
.await;
let sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
// The message from Bob is delivered late, Bob is already removed.
let msg = alice.recv_msg(&sent).await;
assert_eq!(msg.text, "Hi hi!");
assert_eq!(msg.error.unwrap(), "Unknown sender for this chat.");
Ok(())
}
}

View File

@@ -222,9 +222,7 @@ impl BobState {
/// This creates an info message in the chat being joined.
async fn notify_peer_verified(&self, context: &Context) -> Result<()> {
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
let msg = stock_str::contact_verified(context, &contact).await;
let chat_id = self.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
if context
.get_config_bool(Config::VerifiedOneOnOneChats)

View File

@@ -149,7 +149,7 @@ pub enum StockMessage {
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))]
#[strum(props(fallback = "Unknown sender for this chat."))]
UnknownSenderForChat = 72,
#[strum(props(fallback = "Message from %1$s"))]
@@ -824,6 +824,7 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
}
/// Stock string: `%1$s verified.`.
#[allow(dead_code)]
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = &contact.get_name_n_addr();
translated(context, StockMessage::ContactVerified)
@@ -929,7 +930,7 @@ pub(crate) async fn welcome_message(context: &Context) -> String {
translated(context, StockMessage::WelcomeMessage).await
}
/// Stock string: `Unknown sender for this chat. See 'info' for more details.`.
/// Stock string: `Unknown sender for this chat.`.
pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
translated(context, StockMessage::UnknownSenderForChat).await
}

View File

@@ -5,11 +5,12 @@ use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatVisibility};
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::ContactId;
use crate::context::Context;
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
@@ -41,34 +42,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 },
AlterChat {
id: chat::SyncId,
action: chat::SyncAction,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum SyncDataOrUnknown {
SyncData(SyncData),
Unknown(serde_json::Value),
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct SyncItem {
timestamp: i64,
data: SyncData,
data: SyncDataOrUnknown,
}
#[derive(Debug, Deserialize)]
@@ -76,6 +71,12 @@ pub(crate) struct SyncItems {
items: Vec<SyncItem>,
}
impl From<SyncData> for SyncDataOrUnknown {
fn from(sync_data: SyncData) -> Self {
Self::SyncData(sync_data)
}
}
impl Context {
/// Adds an item to the list of items that should be synchronized to other devices.
///
@@ -92,7 +93,10 @@ impl Context {
return Ok(());
}
let item = SyncItem { timestamp, data };
let item = SyncItem {
timestamp,
data: data.into(),
};
let item = serde_json::to_string(&item)?;
self.sql
.execute("INSERT INTO multi_device_sync (item) VALUES(?);", (item,))
@@ -104,7 +108,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<chat::ChatId>) -> Result<()> {
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
if !self.get_config_bool(Config::SyncMsgs).await? {
return Ok(());
}
@@ -155,7 +159,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 =
chat::ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
.await?;
let mut msg = Message {
chat_id,
@@ -248,41 +252,50 @@ impl Context {
/// as otherwise we would add in a dead-loop between two devices
/// sending message back and forth.
///
/// If an error is returned, the caller shall not try over.
/// Therefore, errors should only be returned on database errors or so.
/// If eg. just an item cannot be deleted,
/// that should not hold off the other items to be executed.
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) -> Result<()> {
/// If an error is returned, the caller shall not try over because some sync items could be
/// already executed. Sync items are considered independent and executed in the given order but
/// regardless of whether executing of the previous items succeeded.
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) {
info!(self, "executing {} sync item(s)", items.items.len());
for item in &items.items {
match &item.data {
AddQrToken(token) => {
let chat_id = if let Some(grpid) = &token.grpid {
if let Some((chat_id, _, _)) =
chat::get_chat_id_by_grpid(self, grpid).await?
{
Some(chat_id)
} else {
warn!(
self,
"Ignoring token for nonexistent/deleted group '{}'.", grpid
);
continue;
}
} else {
None
};
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber)
.await?;
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
SyncDataOrUnknown::SyncData(data) => match data {
AddQrToken(token) => self.add_qr_token(token).await,
DeleteQrToken(token) => self.delete_qr_token(token).await,
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
},
SyncDataOrUnknown::Unknown(data) => {
warn!(self, "Ignored unknown sync item: {data}.");
Ok(())
}
DeleteQrToken(token) => {
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?,
}
.log_err(self)
.ok();
}
}
async fn add_qr_token(&self, token: &QrTokenData) -> Result<()> {
let chat_id = if let Some(grpid) = &token.grpid {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(self, grpid).await? {
Some(chat_id)
} else {
warn!(
self,
"Ignoring token for nonexistent/deleted group '{}'.", grpid
);
return Ok(());
}
} else {
None
};
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber).await?;
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
Ok(())
}
async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> {
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
token::delete(self, Namespace::Auth, &token.auth).await?;
Ok(())
}
}
@@ -292,10 +305,9 @@ mod tests {
use std::time::{Duration, SystemTime};
use anyhow::bail;
use strum::IntoEnumIterator;
use super::*;
use crate::chat::{Chat, ProtectionStatus};
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::contact::{Contact, Origin};
use crate::test_utils::TestContext;
@@ -319,13 +331,13 @@ 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.
// Having one test on `SyncData::AlterChat` is sufficient here as
// `chat::SyncAction::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(
id: chat::SyncId::ContactAddr("bob@example.net".to_string()),
action: chat::SyncAction::SetMuted(chat::MuteDuration::Until(
SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
)),
},
@@ -394,54 +406,41 @@ mod tests {
assert!(t.parse_sync_items(r#"{"badname":[]}"#.to_string()).is_err());
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#
.to_string(),
)
.is_err());
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#.to_string(),
)
.is_err()); // `123` is invalid for `String`
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#.to_string(),
)
.is_err()); // `true` is invalid for `String`
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#.to_string(),
)
.is_err()); // `[]` is invalid for `String`
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#.to_string(),
)
.is_err()); // `{}` is invalid for `String`
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#.to_string(),
)
.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
for bad_item_example in [
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#,
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#, // `123` is invalid for `String`
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#, // `true` is invalid for `String`
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#, // `[]` is invalid for `String`
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#, // `{}` is invalid for `String`
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#, // missing field
r#"{"items":[{"timestamp":1631781316,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#, // Unknown enum value
] {
let sync_items = t.parse_sync_items(bad_item_example.to_string()).unwrap();
assert_eq!(sync_items.items.len(), 1);
assert!(matches!(sync_items.items[0].timestamp, 1631781316));
assert!(matches!(
sync_items.items[0].data,
SyncDataOrUnknown::Unknown(_)
));
}
// 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 {
let SyncDataOrUnknown::SyncData(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!(
*id,
chat::SyncId::ContactAddr("bob@example.net".to_string())
);
assert_eq!(
*action,
ChatAction::SetMuted(chat::MuteDuration::Until(
chat::SyncAction::SetMuted(chat::MuteDuration::Until(
SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
))
);
@@ -474,7 +473,9 @@ mod tests {
)?;
assert_eq!(sync_items.items.len(), 1);
if let AddQrToken(token) = &sync_items.items.get(0).unwrap().data {
if let SyncDataOrUnknown::SyncData(AddQrToken(token)) =
&sync_items.items.get(0).unwrap().data
{
assert_eq!(token.invitenumber, "in");
assert_eq!(token.auth, "yip");
assert_eq!(token.grpid, None);
@@ -512,7 +513,7 @@ mod tests {
.to_string(),
)
?;
t.execute_sync_items(&sync_items).await?;
t.execute_sync_items(&sync_items).await;
assert!(
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
@@ -546,7 +547,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 = chat::ChatId::create_for_contact(&alice, ContactId::SELF).await?;
let chat_id = 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);
@@ -569,124 +570,4 @@ 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

@@ -580,7 +580,7 @@ impl TestContext {
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
// origin when creating this contact.
let (contact_id, modified) =
Contact::add_or_lookup(self, &name, addr, Origin::MailinglistAddress)
Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress)
.await
.expect("add_or_lookup");
match modified {

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::{Chat, ProtectionStatus};
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::DC_GCL_FOR_FORWARDING;
@@ -126,24 +126,22 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
VerifiedStatus::BidirectVerified
);
// As soon as Alice creates a chat with Fiona, it should directly be protected
// Alice should have a hidden protected chat with Fiona
{
let chat = alice.create_chat(&fiona).await;
let chat = alice.get_chat(&fiona).await;
assert!(chat.is_protected());
let msg = alice.get_last_msg().await;
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
let expected_text = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg.text, expected_text);
}
// Fiona should also see the chat as protected
// Fiona should have a hidden protected chat with Alice
{
let rcvd = tcm.send_recv(&alice, &fiona, "Hi Fiona").await;
let alice_fiona_id = rcvd.chat_id;
let chat = Chat::load_from_db(&fiona, alice_fiona_id).await?;
let chat = fiona.get_chat(&alice).await;
assert!(chat.is_protected());
let msg0 = get_chat_msg(&fiona, chat.id, 0, 2).await;
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await;
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
assert_eq!(msg0.text, expected_text);
}
@@ -165,6 +163,15 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
let msg1 = get_chat_msg(&alice, chat.id, 0, 3).await;
assert_eq!(msg1.get_info_type(), SystemMessage::ChatProtectionEnabled);
let msg2 = get_chat_msg(&alice, chat.id, 1, 3).await;
assert_eq!(msg2.get_info_type(), SystemMessage::ChatProtectionDisabled);
let msg2 = get_chat_msg(&alice, chat.id, 2, 3).await;
assert_eq!(msg2.text, "I have a new device");
// After recreating the chat, it should still be unprotected
chat.id.delete(&alice).await?;
@@ -705,6 +712,39 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
Ok(())
}
/// Regression test for the following bug:
///
/// - Scan your chat partner's QR Code
/// - They change devices
/// - Scan their QR code again
///
/// -> The re-verification fails.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verify_then_verify_again() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
tcm.section("Bob reinstalls DC");
drop(bob);
let bob_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&bob_new]).await;
bob_new.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob_new).await?;
tcm.execute_securejoin(&bob_new, &alice).await;
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
Ok(())
}
/// Regression test:
/// - Verify a contact
/// - The contact stops using DC and sends a message from a classical MUA instead

View File

@@ -17,7 +17,7 @@ Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162][])
Client/server identification | IMAP ID extension ([RFC 2971][])
Authorization | OAuth2 ([RFC 6749][])
End-to-end encryption | [Autocrypt Level 1][], OpenPGP ([RFC 4880][]), Security Multiparts for MIME ([RFC 1847][]) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
Detect/prevent active attacks | [countermitm][] protocols
Detect/prevent active attacks | [securejoin][] protocols
Compare public keys | [openpgp4fpr][] URI Scheme
Header encryption | [Protected Headers for Cryptographic E-mail](https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/)
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][]
@@ -29,7 +29,7 @@ Return receipts | Message Disposition Notification (MDN, [RFC 8
Locations | KML ([Open Geospatial Consortium](http://www.opengeospatial.org/standards/kml/), [Google Dev](https://developers.google.com/kml/))
[Autocrypt Level 1]: https://autocrypt.org/level1.html
[countermitm]: https://countermitm.readthedocs.io/en/latest/
[securejoin]: https://securejoin.readthedocs.io/en/latest/
[openpgp4fpr]: https://metacode.biz/openpgp/openpgp4fpr
[Autodiscover]: https://learn.microsoft.com/en-us/exchange/autodiscover-service-for-exchange-2013
[XEP-0392]: https://xmpp.org/extensions/xep-0392.html

View File

@@ -0,0 +1,8 @@
Group#Chat#10: Group chat [3 member(s)]
--------------------------------------------------------------------------------
Msg#10: (Contact#Contact#11): I created a group [FRESH]
Msg#11: (Contact#Contact#11): Member Fiona (fiona@example.net) added by alice@example.org. [FRESH][INFO]
Msg#12: Me (Contact#Contact#Self): You removed member Fiona (fiona@example.net). [INFO] o
Msg#13: info (Contact#Contact#Info): Member Fiona (fiona@example.net) added. [NOTICED][INFO]
Msg#14: (Contact#Contact#11): Welcome, Fiona! [FRESH]
--------------------------------------------------------------------------------

View File

@@ -2,6 +2,6 @@ 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]
Msg#12: info (Contact#Contact#Info): Member Me (bob@example.net) added. [NOTICED][INFO]
Msg#13: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
--------------------------------------------------------------------------------