Compare commits

...
Author SHA1 Message Date
Hocuri 61fb9e7e3b test: Revert Mark receive_imf() as only for tests and "internals" feature
Unfortunately, this made compiling the `receive_emails` benchmark fail because it depends on `receive_imf()`, and I don't think there is a nice way to enable this function for the benchmark.
2024-11-28 15:44:33 +01:00
bjoernandiequidoo da546d3526 docs: update dc_msg_get_info_type() and dc_get_securejoin_qr() (#6269)
this was partly missing at
https://github.com/deltachat/deltachat-core-rust/pull/6223

this is not meant as being exhaustive :)

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-11-28 07:19:48 +00:00
link2xt 6be96d3eba refactor: remove some .unwrap() calls 2024-11-27 23:57:23 +00:00
bjoern d1537095e4 chore(release): prepare for 1.151.2 (#6267)
following `RELEASE.md`, after merging, the following is needed:

6. Tag the release: `git tag --annotate v1.151.2`.
7. Push the release tag: `git push origin v1.151.2`.
8. Create a GitHub release: `gh release create v1.151.2 --notes ''`.
2024-11-27 13:37:01 +01:00
bjoern ba68b87c58 feat: add href to IncomingWebxdcNotify event (#6266)
this PR adds the `href` from `update.href` to the IncomingWebxdcNotify
event (DC_EVENT_INCOMING_WEBXDC_NOTIFY in cffi)

purpose is to add a "Start" button to the notifications that allow
starting the app immediately with the given href
2024-11-26 18:21:09 +01:00
B. Petersen b5f899540c change update.notify to a dict of addr:text_to_notify and allow to notify all using the special addr '*' 2024-11-26 14:10:00 +01:00
B. Petersen c6dd03590c feat: add webxdc limits api 2024-11-26 14:09:40 +01:00
link2xt ff3efafcfc fix: revert treating some transient SMTP errors as permanent 2024-11-26 03:08:40 +00:00
iequidoo 717c18ed0f test: Check that IncomingMsg isn't emitted for reactions 2024-11-25 20:58:45 -03:00
bjoern 4026c827be prefer long options in RELEASE.md (#6136)
using long options make things less mystical, it is clearer what
happens.

(apart from that i also disallowed `git -a` on my machine in general, as
`git commit -a` is considered harmful. as my approach to disallow that
is a bit greedy and disallows `-a` just for any git commands, this is
the only place where i regularly struggle :)
2024-11-25 17:34:20 +01:00
l cd8cff7efb feat: do not use format=flowed in outgoing messages (#6256)
Text parts are using quoted-printable encoding
which takes care of wrapping long lines,
so using format=flowed is unnecessary.

This improves compatibility with receivers
which do not support format=flowed.

Receiving format=flowed messages is still possible, receiver side of
Delta Chat is unchanged.
2024-11-25 15:40:38 +00:00
Simon Laux a319c1ea27 feat: add AccountsChanged and AccountsItemChanged events (#6118)
- **feat: add `AccountsChanged` and `AccountsItemChanged` events**
- **emit event and add tests**

closes #6106

TODO:
- [x] test receiving synced config from second device
- [x] bug: investigate how to delay the configuration event until it is
actually configured - because desktop gets the event but still shows
account as if it was unconfigured, maybe event is emitted before the
value is written to the database?
- [x] update node bindings constants
2024-11-25 13:34:33 +00:00
iequidoo 5db574b44f refactor: create_status_update_record: Get rid of notify var
It's used in the only place. Also this way `get_webxdc_self_addr()` which makes a db query is only
called when necessary.
2024-11-25 11:18:07 +01:00
iequidoo 8af90a1299 feat: AEAP: Check that the old peerstate verified key fingerprint hasn't changed when removing it 2024-11-24 15:51:19 -03:00
bjoern a6db7ba1e3 api: deprecate webxdc descr parameter (#6255)
this PR removes most usages of the `descr` parameter.

- to avoid noise in different branches etc. (as annoying on similar, at
a first glance simple changes), i left the external API stable

- also, the effort to do a database migration seems to be over the top,
so the column is left and set to empty strings on future updates - maybe
we can recycle the column at some point ;)

closes #6245
2024-11-24 16:34:24 +00:00
link2xt 703cad970d chore(release): prepare for 1.151.1 2024-11-24 14:00:53 +00:00
link2xt 47757c3c7f ci: test building nix targets to avoid regressions
Otherwise build failure may only be detected during release.
2024-11-24 13:45:06 +00:00
link2xt dca922b932 build(nix): fix deltachat-rpc-server-source installable 2024-11-24 13:45:06 +00:00
link2xt bacdf8f8df chore(release): prepare for 1.151.0 2024-11-23 21:57:19 +00:00
link2xt eed2320217 build: use underscores in deltachat-rpc-server source package filename 2024-11-23 21:49:20 +00:00
iequidoo d22c29ab89 test: After AEAP, 1:1 chat isn't available for sending, but unprotected groups are (#6222) 2024-11-23 18:34:18 -03:00
bjoern 22b9308c9b feat: update.href api (#6248)
add `update.href` property option to update objects send via
`Context::send_webxdc_status_update()`.

when set together with `update.info`,
UI can implement the info message as a link that is passed to the webxdc
via `window.location.href`.
for that purpose, UI will read the link back from
`Message::get_webxdc_href()`.

Practically,
this allows e.g. an calendar.xdc
to emits clickable update messages
opening the calendar at the correct date.

closes #6219

documentation at https://github.com/webxdc/website/pull/90
2024-11-23 18:38:02 +01:00
bjoern 1f0a12a729 fix: never notify SELF (#6251)
it may be handy for an xdc to have only one list of all adresses, or
there may just be bugs.

in any case, do not notify SELF, e.g. in a multi-device setup; we're
also not doing this for other messages.

this is also a preparation for having an option to notify ALL.
2024-11-23 18:16:20 +01:00
dependabot[bot] d06fa73e4f chore(deps): bump curve25519-dalek from 3.2.0 to 4.1.3 in /fuzz
Bumps [curve25519-dalek](https://github.com/dalek-cryptography/curve25519-dalek) from 3.2.0 to 4.1.3.
- [Release notes](https://github.com/dalek-cryptography/curve25519-dalek/releases)
- [Commits](https://github.com/dalek-cryptography/curve25519-dalek/compare/3.2.0...curve25519-4.1.3)

---
updated-dependencies:
- dependency-name: curve25519-dalek
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-23 15:53:12 +00:00
adb 407bc95ae5 remove imap_tools from dependencies (#6238) 2024-11-23 16:28:23 +01:00
Hocuri daeeca3710 docs: Clarify DC_EVENT_INCOMING_WEBXDC_NOTIFY documentation (#6249)
I found the old documentation rather hard to understand. The new doc
string:
- uses whole sentences, leaving less space for misinterpretation
- explicitly mentions that it can happen that there is no
webxdc-info-message
- is clearly structured using bullet points.
2024-11-23 15:52:08 +01:00
29de7c3603 feat: webxdc notify (#6230)
this PR adds support for the property `update.notify` to notify about
changes in `update.info` or `update.summary`. the property can be set to
an array of addresses [^1]

core emits then the event `IncomingWebxdcNotify`, resulting in all UIs
to display a system notification, maybe even via PUSH.

for using the existing `update.info` and `update.summary`: the message
is no secret and should be visible to all group members as usual, to not
break the UX of having same group messages on all devices of all users -
as known already from the normal messages.

also, that way, there is no question what happens if user have disabled
notifications as the change is presented in the chat as well

doc counterpart at https://github.com/webxdc/website/pull/90

closes #6217 

[^1]: addresses come in either via the payload as currently or as an
explicit sender in the future - this does not affect this PR. same for
translations, see discussions at #6217 and #6097

---------

Co-authored-by: adb <asieldbenitez@gmail.com>
Co-authored-by: l <link2xt@testrun.org>
2024-11-22 21:31:56 +01:00
link2xt f669f43fe6 chore(cargo): update Rustls from 0.23.14 to 0.23.18 2024-11-22 18:50:25 +00:00
bjoern 8a0c913bbd feat: use privacy-preserving webxdc addresses (#6237)
this PR adds the address to be used by the UI for
`window.webxdc.selfAddr` to webxdc-info. UIs need to be changed
accordingly and must not use configured_addr any longer.

the address is created by sha256(private-key + rfc724_mid) , which
results in different addresses for each webxdc, without the option to
find out the real address of the user.

this also returns the same address for a multi-device-setup - sending
totally random self address around might be an alternative, however
would require connectivity (both devices may be offline on first start).

for existing app, after the change, there will be a new user, resulting
eg. in a new highscore, otherwise, things should be mostly fine. this
assumption is also important as we might change the thing another time
when it comes to multi-transport.

ftr, addresses look like
`0f187e3f420748b03e3da76543e9a84ecff822687ce7e94f250c04c7c50398bc` now

when this is merged, we need to adapt #6230 and file issues for all UI
to use `info.selfAddr`

closes #6216
2024-11-21 18:00:29 +00:00
link2xt 75e1517dcc feat: trim whitespace from scanned QR codes 2024-11-21 17:50:21 +00:00
link2xt 4aad8fb3de docs: move style guide into a separate document
Code contribution guidelines
are rearranged into a list of steps to follow.
2024-11-21 15:49:21 +00:00
link2xt 9640f92327 chore(release): prepare for 1.150.0 2024-11-21 14:42:43 +00:00
Hocuri 95ac7647ac test: Mark receive_imf() as only for tests and "internals" feature (#6235)
`receive_imf() is only used in tests and the REPL, which enables the
"internals" feature. This PR marks it as such, so that it's clear not
only from the comment that this function is not used for anything else.
2024-11-21 14:57:35 +01:00
link2xt e121fc1389 refactor: delete chat in a transaction 2024-11-20 18:12:50 +00:00
iequidoo 5399cbfffe fix: Update state of message when fully downloading it
If a message partially downloaded before is already IMAP-seen upon a full download, it should be
updated to `InSeen`. OTOH if it's not IMAP-seen, but already `InNoticed` locally, its state should
be preserved. So we take the maximum of two states.
2024-11-20 14:27:24 -03:00
iequidoo 8da1fae51f fix: markseen_msgs: Limit not yet downloaded messages state to InNoticed (#2970)
This fixes sending MDNs for big messages when they are downloaded and really seen. Otherwise MDNs
are not sent for big encrypted messages because they "don't want MDN" until downloaded.
2024-11-20 14:27:24 -03:00
iequidoo eabf1d15b7 test: Mark not downloaded message as seen (#2970)
Add a test on what happens currently when apps call `markseen_msgs()` for not downloaded encrypted
messages. Such messages are marked as seen, but MDNs aren't sent for them. Also currently when such
a message is downloaded, it remains `InSeen` despite the full content hasn't yet been seen by the
user.
2024-11-20 14:27:24 -03:00
gerryfrancis 3b9e6d6ffa Fix type in context.rs 2024-11-19 18:35:42 +01:00
Sebastian Klähn 8f3be764d2 change: Use i.delta.chat in qr codes (#6223)
As discussed in #5467 we want to use `i.delta.chat` in QR codes in favor
of `OPENPGP4FPR:` scheme. This PR does the replacement in
`get_securejoin_qr` which is used in `get_securejoin_qr_svg`.

close #5467
2024-11-19 17:32:42 +01:00
Hocuri c181db631f feat: Clear config cache in start_io() (#6228)
This is needed for iOS (https://github.com/deltachat/deltachat-ios/pull/2393), see comment in the code. An alternative would be
to add an API `invalidate_config_cache()` or to do nothing and just
assume that things will be fine.
2024-11-19 15:59:05 +00:00
link2xt c18a476806 refactor: forbid clippy::string_slice 2024-11-18 23:57:57 +00:00
link2xt 3235c8bc9f refactor: forbid clippy::indexing_slicing
It is impossible to allow this in the new code now.
2024-11-18 21:58:48 +00:00
link2xt a5d336fafc refactor: remove unused allow(clippy::indexing_slicing) from 'truncate' 2024-11-18 21:58:48 +00:00
link2xt 5ebca15502 refactor: get rid of slicing in remove_top_quote 2024-11-18 21:58:48 +00:00
link2xt d0b945d4ee refactor: remove slicing from remove_bottom_quote 2024-11-18 21:58:48 +00:00
link2xt d3d2509273 refactor: remove indexing/slicing from parse_message_ids 2024-11-18 21:58:48 +00:00
link2xt 1db6370d6a refactor: remove unused allow(clippy::indexing_slicing) for heuristically_parse_ndn 2024-11-18 21:58:48 +00:00
link2xt dc58e11d13 refactor: remove indexing/slicing from squash_attachment_parts 2024-11-18 21:58:48 +00:00
link2xt 442e2787c6 refactor: remove indexing/slicing from remove_message_footer 2024-11-18 21:58:48 +00:00
link2xt 7b1fa50fb0 refactor: remove unused allow(clippy::indexing_slicing) 2024-11-18 21:58:48 +00:00
link2xt 2315be2c90 refactor: eliminate indexing in compute_mailinglist_name 2024-11-18 21:58:48 +00:00
link2xt 41478e1e48 refactor: do not use slicing in qr module 2024-11-18 21:58:48 +00:00
link2xt 9e13486143 refactor: don't use slicing in remove_nonstandard_footer 2024-11-18 21:58:48 +00:00
link2xt 06eea7ebe8 refactor: remove unnecessary allow(clippy::indexing_slicing)
clippy::indexing_slicing is already allowed in test builds.
2024-11-18 21:58:48 +00:00
link2xt 514f0296c0 refactor: remove slicing from is_file_in_use
There is a change in behavior for the case
when name is the same as the suffix
(`name_len` == `namespc_len`),
but normally `files_in_use` should not contain empty filenames.
2024-11-18 21:58:48 +00:00
Sebastian Klähnandl 399716a761 Fix: Dont overwrite equal drafts (#6212)
This PR prevents overwriting drafts when the text and file are the same.

close #6211

---------

Co-authored-by: l <link2xt@testrun.org>
2024-11-17 08:54:50 +00:00
B. Petersen 60163cb121 docs: scanned proxies are added and normalized
there was a bug on iOS before,
that assumed that the proxy needs to be added to the proxy list additionally,
also the normalization was unexpected.
2024-11-16 11:00:42 +01:00
link2xt e117efa744 ci: ensure flake is formatted 2024-11-15 10:23:36 +00:00
link2xt 7b98274681 fix(deltachat-jsonrpc): do not fail get_draft if draft is deleted 2024-11-14 19:51:43 +00:00
link2xt ea385fabae fix(deltachat-jsonrpc): do not fail get_chatlist_items_by_entries if the message got deleted
The message may be deleted while chatlist item is loading.
In this case displaying "No messages" is better than failing.
Ideally loading of the chatlist item
should happen in 1 database transaction and
always return some message if chat is not empty,
but this requires large refactoring.
2024-11-14 19:51:43 +00:00
link2xt 3a976a8580 fix: do not fail to load chatlist summary if the message got removed 2024-11-14 19:51:43 +00:00
link2xt e7a29f0aa7 chore(cargo): update rPGP from 0.13.2 to 0.14.0 2024-11-14 09:31:40 +00:00
bjoernandiequidoo 010b655ee9 api: correct DC_CERTCK_ACCEPT_* values and docs (#6176)
this PR changes `DC_CERTCK_ACCEPT_*` to the same values in cffi as rust
does. and regards the same values as deprecated afterwards

there is some confusion about what is deprecated and what not, see
https://github.com/deltachat/deltachat-android/issues/3408

iOS needs to be adapted as it was following the docs in the CFFI before,
same desktop. both need to be graceful on reading and strict on writing.

~~**this PR is considered harmful,** so we should not merge that in
during 1.48 release, there is no urgency, things are fine (wondering if
it isn't even worth the effort, however, having different values and
deprecations is a call for trouble in the future ...)~~

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-11-13 16:46:32 +00:00
B. Petersen fe53eb2b37 feat: tune down io-not-started info in connectivity-html
due to async processing,
it may happen getConnectivityHtml() is called from UI before startIO() is actually called.
eg. on iOS, we may delay startIo() if another process is still processing a PUSH notification -
when during this time, the connectivity view is opened,
it is weird if a big error "CONTACT THE DEVELOPERS!11!!!" is shown :)

also, there is not really a function is_connected(),
for $reasons, as this turned out to be flacky,
so it is not even easy to check the state before calling getConnectivityHtml()

it is not worth in doing too much special,
we are talking about rare situaton,
also, the connectivity view gets updated some moments later.
2024-11-13 13:20:00 +01:00
Sebastian Klähn 9c0e932e39 update flake.nix (#6200)
Before I was getting
```
error: attribute 'targetPlatforms' missing
at /nix/store/dyzl40h25l04565n90psbhzgnc5vp2xr-source/pkgs/build-support/rust/build-rust-package/default.nix:162:7:
  161|       meta.platforms or lib.platforms.all
  162|       rustc.targetPlatforms;
     |       ^
  163|   };
```
This was probably an upstream issues as discussed in here
https://discourse.nixos.org/t/error-attribute-targetplatforms-missing-after-updating-inputs/54494

After this update it is fixed.
2024-11-13 09:56:19 +01:00
iequidoo 19dc16d9d3 test: Reply to protected group from MUA
This must be possible if a message is properly signed and encrypted.
2024-11-11 14:35:00 -03:00
B. Petersen 302acb218f add a test for is_quote_headline() 2024-11-11 17:26:32 +01:00
B. Petersen a9b71aff6d line-before-quote may be up to 120 character long.
80 characters are a bit limited in practise ...

On Mon, 3 Jan, 2022 at 8:34 PM "Anonymous The Mighty" <anonymous@example.com> wrote:

... already breaks the limit. it is good to allow up to 40 additional characters
for name + email address.

allowing any length, however, may catch too much,
as the line could also be a normal paragraph with important content,
so 120 characters seems reasonable.

the idea of adding more complexity here would probably lead only to, well more complexity -
things can anyways go wrong -
and, we have the "show full message..." button for exactly that purpose,
so that the user can access everything as original.

so, if things go wrong sometimes,
this is expected and fine.
2024-11-11 17:26:32 +01:00
link2xt 1e886a34f0 chore: remove some duplicate changelog entries
dc_chatlist_get_summary2() was added in 1.41.0
2024-11-11 15:09:06 +00:00
link2xt 99330dd2de chore(cargo): update futures-concurrency from 7.6.1 to 7.6.2 2024-11-11 12:42:03 +00:00
link2xt 1412ffd771 build: silence RUSTSEC-2024-0384 2024-11-11 12:39:03 +00:00
Sebastian Klähn 6b2d49acb8 Copy over some docs as requested in the associated issue. (#6193)
Copy over some docs as requested in the associated issue.

close #5503
2024-11-10 23:30:43 +01:00
l 3b2f18f926 feat: use Rustls for connections with strict TLS (#6186) 2024-11-07 19:07:11 +00:00
iequidoo c9cf2b7f2e fix: Only add "member added/removed" messages if they actually do that (#5992)
There were many cases in which "member added/removed" messages were added to chats even if they
actually do nothing because a member is already added or removed. But primarily this fixes a
scenario when Alice has several devices and shares an invite link somewhere, and both their devices
handle the SecureJoin and issue `ChatGroupMemberAdded` messages so all other members see a
duplicated group member addition.
2024-11-07 14:29:09 -03:00
93 changed files with 3458 additions and 2441 deletions
+104
View File
@@ -0,0 +1,104 @@
name: Test Nix flake
on:
pull_request:
paths:
- flake.nix
- flake.lock
push:
paths:
- flake.nix
- flake.lock
branches:
- main
jobs:
format:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix fmt
# Check that formatting does not change anything.
- run: git diff --exit-code
build:
name: nix build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
installable:
# Ensure `nix develop` will work.
- devShells.x86_64-linux.default
- deltachat-python
- deltachat-repl
- deltachat-repl-aarch64-linux
- deltachat-repl-arm64-v8a-android
- deltachat-repl-armeabi-v7a-android
- deltachat-repl-armv6l-linux
- deltachat-repl-armv7l-linux
- deltachat-repl-i686-linux
- deltachat-repl-win32
- deltachat-repl-win64
- deltachat-repl-x86_64-linux
- deltachat-rpc-client
- deltachat-rpc-server
- deltachat-rpc-server-aarch64-linux
- deltachat-rpc-server-aarch64-linux-wheel
- deltachat-rpc-server-arm64-v8a-android
- deltachat-rpc-server-armeabi-v7a-android
- deltachat-rpc-server-armv6l-linux
- deltachat-rpc-server-armv6l-linux-wheel
- deltachat-rpc-server-armv7l-linux
- deltachat-rpc-server-armv7l-linux-wheel
- deltachat-rpc-server-i686-linux
- deltachat-rpc-server-i686-linux-wheel
- deltachat-rpc-server-source
- deltachat-rpc-server-win32
- deltachat-rpc-server-win32-wheel
- deltachat-rpc-server-win64
- deltachat-rpc-server-win64-wheel
- deltachat-rpc-server-x86_64-linux
- deltachat-rpc-server-x86_64-linux-wheel
- docs
- libdeltachat
- python-docs
# Fails to build
#- deltachat-repl-x86_64-android
#- deltachat-repl-x86-android
#- deltachat-rpc-server-x86_64-android
#- deltachat-rpc-server-x86-android
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build .#${{ matrix.installable }}
build-macos:
name: nix build on macOS
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
installable:
- deltachat-rpc-server-aarch64-darwin
# Fails to bulid
# - deltachat-rpc-server-x86_64-darwin
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build .#${{ matrix.installable }}
+140 -4
View File
@@ -1,5 +1,141 @@
# Changelog
## [1.151.2] - 2024-11-26
### API-Changes
- Deprecate webxdc `descr` parameter ([#6255](https://github.com/deltachat/deltachat-core-rust/pull/6255)).
### Features / Changes
- AEAP: Check that the old peerstate verified key fingerprint hasn't changed when removing it.
- Add `AccountsChanged` and `AccountsItemChanged` events ([#6118](https://github.com/deltachat/deltachat-core-rust/pull/6118)).
- Do not use format=flowed in outgoing messages ([#6256](https://github.com/deltachat/deltachat-core-rust/pull/6256)).
- Add webxdc limits api.
- Add href to IncomingWebxdcNotify event ([#6266](https://github.com/deltachat/deltachat-core-rust/pull/6266)).
### Fixes
- Revert treating some transient SMTP errors as permanent.
### Refactor
- Create_status_update_record: Get rid of `notify` var.
### Tests
- Check that IncomingMsg isn't emitted for reactions.
## [1.151.1] - 2024-11-24
### Build system
- nix: Fix deltachat-rpc-server-source installable.
### CI
- Test building nix targets to avoid regressions.
## [1.151.0] - 2024-11-23
### Features / Changes
- Trim whitespace from scanned QR codes.
- Use privacy-preserving webxdc addresses ([#6237](https://github.com/deltachat/deltachat-core-rust/pull/6237)).
- Webxdc notify ([#6230](https://github.com/deltachat/deltachat-core-rust/pull/6230)).
- `update.href` api ([#6248](https://github.com/deltachat/deltachat-core-rust/pull/6248)).
### Fixes
- Never notify SELF ([#6251](https://github.com/deltachat/deltachat-core-rust/pull/6251)).
### Build system
- Use underscores in deltachat-rpc-server source package filename.
- Remove imap_tools from dependencies ([#6238](https://github.com/deltachat/deltachat-core-rust/pull/6238)).
- cargo: Update Rustls from 0.23.14 to 0.23.18.
- deps: Bump curve25519-dalek from 3.2.0 to 4.1.3 in /fuzz.
### Documentation
- Move style guide into a separate document.
- Clarify DC_EVENT_INCOMING_WEBXDC_NOTIFY documentation ([#6249](https://github.com/deltachat/deltachat-core-rust/pull/6249)).
### Tests
- After AEAP, 1:1 chat isn't available for sending, but unprotected groups are ([#6222](https://github.com/deltachat/deltachat-core-rust/pull/6222)).
## [1.150.0] - 2024-11-21
### API-Changes
- Correct `DC_CERTCK_ACCEPT_*` values and docs ([#6176](https://github.com/deltachat/deltachat-core-rust/pull/6176)).
### Features / Changes
- Use Rustls for connections with strict TLS ([#6186](https://github.com/deltachat/deltachat-core-rust/pull/6186)).
- Experimental header protection for Autocrypt.
- Tune down io-not-started info in connectivity-html.
- Clear config cache in start_io() ([#6228](https://github.com/deltachat/deltachat-core-rust/pull/6228)).
- Line-before-quote may be up to 120 character long instead of 80.
- Use i.delta.chat in qr codes ([#6223](https://github.com/deltachat/deltachat-core-rust/pull/6223)).
### Fixes
- Prevent accidental wrong-password-notifications ([#6122](https://github.com/deltachat/deltachat-core-rust/pull/6122)).
- Remove footers from "Show Full Message...".
- `send_msg_to_smtp`: Return Ok if `smtp` row is deleted in parallel.
- Only add "member added/removed" messages if they actually do that ([#5992](https://github.com/deltachat/deltachat-core-rust/pull/5992)).
- Do not fail to load chatlist summary if the message got removed.
- deltachat-jsonrpc: Do not fail `get_chatlist_items_by_entries` if the message got deleted.
- deltachat-jsonrpc: Do not fail `get_draft` if draft is deleted.
- `markseen_msgs`: Limit not yet downloaded messages state to `InNoticed` ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)).
- Update state of message when fully downloading it.
- Dont overwrite equal drafts ([#6212](https://github.com/deltachat/deltachat-core-rust/pull/6212)).
### Build system
- Silence RUSTSEC-2024-0384.
- cargo: Update rPGP from 0.13.2 to 0.14.0.
- cargo: Update futures-concurrency from 7.6.1 to 7.6.2.
- Update flake.nix ([#6200](https://github.com/deltachat/deltachat-core-rust/pull/6200))
### CI
- Ensure flake is formatted.
### Documentation
- Scanned proxies are added and normalized.
### Refactor
- Fix nightly clippy warnings.
- Remove slicing from `is_file_in_use`.
- Remove unnecessary `allow(clippy::indexing_slicing)`.
- Don't use slicing in `remove_nonstandard_footer`.
- Do not use slicing in `qr` module.
- Eliminate indexing in `compute_mailinglist_name`.
- Remove unused `allow(clippy::indexing_slicing)`.
- Remove indexing/slicing from `remove_message_footer`.
- Remove indexing/slicing from `squash_attachment_parts`.
- Remove unused allow(clippy::indexing_slicing) for heuristically_parse_ndn.
- Remove indexing/slicing from `parse_message_ids`.
- Remove slicing from `remove_bottom_quote`.
- Get rid of slicing in `remove_top_quote`.
- Remove unused allow(clippy::indexing_slicing) from 'truncate'.
- Forbid clippy::indexing_slicing.
- Forbid clippy::string_slice.
- Delete chat in a transaction.
- Fix typo in `context.rs`.
### Tests
- Remove all calls to print() from deltachat-rpc-client tests.
- Reply to protected group from MUA.
- Mark not downloaded message as seen ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)).
- Mark `receive_imf()` as only for tests and "internals" feature ([#6235](https://github.com/deltachat/deltachat-core-rust/pull/6235)).
## [1.149.0] - 2024-11-05
### Build system
@@ -4481,14 +4617,10 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
- new qr-code type `DC_QR_WEBRTC` #1779
- new `dc_chatlist_get_summary2()` api #1771
- tweak smtp-timeout for larger mails #1782
- optimize read-receipts #1765
- Allow http scheme for DCACCOUNT URLs #1770
- improve tests #1769
- bug fixes #1766 #1772 #1773 #1775 #1776 #1777
@@ -5233,3 +5365,7 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.148.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.5..v1.148.6
[1.148.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.6..v1.148.7
[1.149.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.7..v1.149.0
[1.150.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.149.0..v1.150.0
[1.151.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.150.0..v1.151.0
[1.151.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.0..v1.151.1
[1.151.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.1..v1.151.2
+101 -165
View File
@@ -1,6 +1,6 @@
# Contributing guidelines
# Contributing to Delta Chat
## Reporting bugs
## Bug reports
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
If the bug you found is specific to
@@ -9,178 +9,114 @@ If the bug you found is specific to
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
report it to the corresponding repository.
## Proposing features
## Feature proposals
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
## Contributing code
## Code contributions
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
If you want to contribute a code, follow this guide.
If you have write access to the repository,
push a branch named `<username>/<feature>`
so it is clear who is responsible for the branch,
and open a PR proposing to merge the change.
Otherwise fork the repository and create a branch in your fork.
1. **Select an issue to work on.**
If you have an write access to the repository, assign the issue to yourself.
Otherwise state in the comment that you are going to work on the issue
to avoid duplicate work.
If the issue does not exist yet, create it first.
2. **Write the code.**
Follow the [coding conventions](STYLE.md) when writing the code.
3. **Commit the code.**
If you have write access to the repository,
push a branch named `<username>/<feature>`
so it is clear who is responsible for the branch,
and open a PR proposing to merge the change.
Otherwise fork the repository and create a branch in your fork.
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
The following prefix types are used:
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
- `test`: Test changes and improvements to the testing framework.
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
Release preparation commits are marked as "chore(release): prepare for X.Y.Z"
as described in [releasing guide](RELEASE.md).
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
Alternatively, breaking changes can go into the commit description, e.g.:
```
fix: Fix race condition and db corruption when a message was received during backup
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
```
4. [**Open a Pull Request**](https://github.com/deltachat/deltachat-core-rust/pulls).
Refer to the corresponding issue.
If you intend to squash merge the PR from the web interface,
make sure the PR title follows the conventional commits notation
as it will end up being a commit title.
Otherwise make sure each commit title follows the conventional commit notation.
5. **Make sure all CI checks succeed.**
CI runs the tests and checks code formatting.
While it is running, self-review your PR to make sure all the changes you expect are there
and there are no accidentally commited unrelated changes and files.
Push the necessary fixup commits or force-push to your branch if needed.
6. **Ask for review.**
Use built-in GitHub feature to request a review from suggested reviewers.
If you do not have write access to the repository, ask for review in the comments.
7. **Merge the PR.**
Once a PR has an approval and passes CI, it can be merged.
PRs from a branch created in the main repository,
i.e. authored by those who have write access, are merged by their authors.
This is to ensure that PRs are merged as intended by the author,
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
If you have multiple changes in one PR, do a rebase merge.
Otherwise, you should usually do a squash merge.
If PR author does not have write access to the repository,
maintainers who reviewed the PR can merge it.
If you do not have access to the repository and created a PR from a fork,
ask the maintainers to merge the PR and say how it should be merged.
## Other ways to contribute
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
You can find the list of good first issues
and a link to this guide
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
### Coding conventions
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
### SQL
Multi-line SQL statements should be formatted using string literals,
for example
```
sql.execute(
"CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '' NOT NULL -- message text
) STRICT",
)
.await?;
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
or [`indoc!](https://docs.rs/indoc).
Do not escape newlines like this:
```
sql.execute(
"CREATE TABLE messages ( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
text TEXT DEFAULT '' NOT NULL \
) STRICT",
)
.await?;
```
Escaping newlines
is prone to errors like this if space before backslash is missing:
```
"SELECT foo\
FROM bar"
```
Literal above results in `SELECT fooFROM bar` string.
This style also does not allow using `--` comments.
---
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
to make SQLite check column types.
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
This avoids reuse of the row IDs and can avoid dangerous bugs
like forwarding wrong message because the message was deleted
and another message took its row ID.
Declare all new columns as `NOT NULL`
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
Use `HAVING COUNT(*) > 0` clause
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
an older version. Also don't change the column type, consider adding a new column with another name
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
keyword doesn't help here.
### Commit messages
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
The following prefix types are used:
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
- `test`: Test changes and improvements to the testing framework.
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
If you intend to squash merge the PR from the web interface,
make sure the PR title follows the conventional commits notation
as it will end up being a commit title.
Otherwise make sure each commit title follows the conventional commit notation.
#### Breaking Changes
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
Alternatively, breaking changes can go into the commit description, e.g.:
```
fix: Fix race condition and db corruption when a message was received during backup
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
```
#### Multiple Changes in one PR
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
[Clippy]: https://doc.rust-lang.org/clippy/
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/
### Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
capitalize it but do not add a full stop as the contexts will be separated by `:`.
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 `?`.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`
and get a backtrace with line numbers in resultified tests
which return `anyhow::Result`.
### Logging
For logging, use `info!`, `warn!` and `error!` macros.
Log messages should be capitalized and have a full stop in the end. For example:
```
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
```
Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
### Reviewing
Once a PR has an approval and passes CI, it can be merged.
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
This is to ensure that PRs are merged as intended by the author,
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
If you do not have access to the repository and created a PR from a fork,
ask the maintainers to merge the PR and say how it should be merged.
## Other ways to contribute
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
Generated
+120 -61
View File
@@ -53,6 +53,15 @@ dependencies = [
"subtle",
]
[[package]]
name = "aes-kw"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c"
dependencies = [
"aes",
]
[[package]]
name = "ahash"
version = "0.8.11"
@@ -551,18 +560,6 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "blake2"
version = "0.10.6"
@@ -1227,7 +1224,7 @@ dependencies = [
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"fiat-crypto 0.2.6",
"rustc_version",
"subtle",
"zeroize",
@@ -1309,7 +1306,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.149.0"
version = "1.151.2"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1375,6 +1372,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sha-1",
"sha2",
"shadowsocks",
"smallvec",
"strum",
@@ -1409,7 +1407,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.149.0"
version = "1.151.2"
dependencies = [
"anyhow",
"async-channel 2.3.1",
@@ -1434,7 +1432,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.149.0"
version = "1.151.2"
dependencies = [
"anyhow",
"deltachat",
@@ -1450,7 +1448,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.149.0"
version = "1.151.2"
dependencies = [
"anyhow",
"deltachat",
@@ -1479,7 +1477,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.149.0"
version = "1.151.2"
dependencies = [
"anyhow",
"deltachat",
@@ -1772,6 +1770,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "ed448-goldilocks"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79"
dependencies = [
"fiat-crypto 0.1.20",
"hex",
"subtle",
]
[[package]]
name = "either"
version = "1.10.0"
@@ -2137,6 +2146,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "fiat-crypto"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
[[package]]
name = "fiat-crypto"
version = "0.2.6"
@@ -2155,6 +2170,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.0.28"
@@ -2211,12 +2232,6 @@ dependencies = [
name = "format-flowed"
version = "1.0.0"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.31"
@@ -2256,11 +2271,11 @@ dependencies = [
[[package]]
name = "futures-concurrency"
version = "7.6.1"
version = "7.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b14ac911e85d57c5ea6eef76d7b4d4a3177ecd15f4bea2e61927e9e3823e19f"
checksum = "d9b724496da7c26fcce66458526ce68fc2ecf4aaaa994281cf322ded5755520c"
dependencies = [
"bitvec",
"fixedbitset",
"futures-buffered",
"futures-core",
"futures-lite 1.13.0",
@@ -3107,7 +3122,7 @@ dependencies = [
"netlink-packet-route",
"netlink-sys",
"netwatch",
"num_enum",
"num_enum 0.7.3",
"once_cell",
"parking_lot",
"pin-project",
@@ -3862,13 +3877,34 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
dependencies = [
"num_enum_derive 0.5.11",
]
[[package]]
name = "num_enum"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
dependencies = [
"num_enum_derive",
"num_enum_derive 0.7.3",
]
[[package]]
name = "num_enum_derive"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
@@ -3877,7 +3913,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate",
"proc-macro-crate 3.2.0",
"proc-macro2",
"quote",
"syn 2.0.86",
@@ -4174,14 +4210,15 @@ dependencies = [
[[package]]
name = "pgp"
version = "0.13.2"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a6c842436d5fa2b59eac1e9b3d142b50bfff99c1744c816b1f4c2ac55a20754"
checksum = "49bb5f77aaf8ae1ed6fe63387ad513b10cd44716fd053ecc227b9493c096cdb2"
dependencies = [
"aes",
"aes-gcm",
"aes-kw",
"argon2",
"base64 0.22.1",
"base64 0.21.7",
"bitfield",
"block-padding",
"blowfish",
@@ -4197,6 +4234,7 @@ dependencies = [
"crc24",
"curve25519-dalek",
"derive_builder",
"derive_more",
"des",
"digest",
"dsa",
@@ -4216,7 +4254,7 @@ dependencies = [
"nom",
"num-bigint-dig",
"num-traits",
"num_enum",
"num_enum 0.5.11",
"ocb3",
"p256",
"p384",
@@ -4233,6 +4271,7 @@ dependencies = [
"thiserror",
"twofish",
"x25519-dalek",
"x448",
"zeroize",
]
@@ -4449,7 +4488,7 @@ dependencies = [
"iroh-metrics",
"libc",
"netwatch",
"num_enum",
"num_enum 0.7.3",
"rand 0.8.5",
"serde",
"smallvec",
@@ -4551,13 +4590,23 @@ dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit 0.19.15",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit",
"toml_edit 0.22.20",
]
[[package]]
@@ -4792,12 +4841,6 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "radix_trie"
version = "0.2.1"
@@ -5227,9 +5270,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.14"
version = "0.23.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f"
dependencies = [
"log",
"once_cell",
@@ -6074,12 +6117,6 @@ version = "4.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "094c9f64d6de9a8506b1e49b63a29333b37ed9e821ee04be694d431b3264c3c5"
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.13.0"
@@ -6363,7 +6400,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
"toml_edit 0.22.20",
]
[[package]]
@@ -6375,6 +6412,17 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"toml_datetime",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.20"
@@ -6385,7 +6433,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
"winnow 0.6.18",
]
[[package]]
@@ -7119,6 +7167,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.18"
@@ -7162,15 +7219,6 @@ dependencies = [
"windows 0.52.0",
]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x25519-dalek"
version = "2.0.1"
@@ -7183,6 +7231,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "x448"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb"
dependencies = [
"ed448-goldilocks",
"hex",
"rand_core 0.5.1",
]
[[package]]
name = "x509-parser"
version = "0.16.0"
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.149.0"
version = "1.151.2"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -76,7 +76,7 @@ num-traits = { workspace = true }
once_cell = { workspace = true }
parking_lot = "0.12"
percent-encoding = "2.3"
pgp = { version = "0.13.2", default-features = false }
pgp = { version = "0.14.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.37"
@@ -92,6 +92,7 @@ serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
shadowsocks = { version = "1.21.0", default-features = false, features = ["aead-cipher-2022"] }
smallvec = "1.13.2"
strum = "0.26"
+2 -2
View File
@@ -14,8 +14,8 @@ For example, to release version 1.116.0 of the core, do the following steps.
5. Commit the changes as `chore(release): prepare for 1.116.0`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
6. Tag the release: `git tag -a v1.116.0`.
6. Tag the release: `git tag --annotate v1.116.0`.
7. Push the release tag: `git push origin v1.116.0`.
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
+98
View File
@@ -0,0 +1,98 @@
# Coding conventions
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
[Clippy]: https://doc.rust-lang.org/clippy/
## SQL
Multi-line SQL statements should be formatted using string literals,
for example
```
sql.execute(
"CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '' NOT NULL -- message text
) STRICT",
)
.await?;
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
or [`indoc!](https://docs.rs/indoc).
Do not escape newlines like this:
```
sql.execute(
"CREATE TABLE messages ( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
text TEXT DEFAULT '' NOT NULL \
) STRICT",
)
.await?;
```
Escaping newlines
is prone to errors like this if space before backslash is missing:
```
"SELECT foo\
FROM bar"
```
Literal above results in `SELECT fooFROM bar` string.
This style also does not allow using `--` comments.
---
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
to make SQLite check column types.
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
This avoids reuse of the row IDs and can avoid dangerous bugs
like forwarding wrong message because the message was deleted
and another message took its row ID.
Declare all new columns as `NOT NULL`
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
Use `HAVING COUNT(*) > 0` clause
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
an older version. Also don't change the column type, consider adding a new column with another name
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
keyword doesn't help here.
## Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
capitalize it but do not add a full stop as the contexts will be separated by `:`.
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 `?`.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`
and get a backtrace with line numbers in resultified tests
which return `anyhow::Result`.
## Logging
For logging, use `info!`, `warn!` and `error!` macros.
Log messages should be capitalized and have a full stop in the end. For example:
```
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
```
Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
Binary file not shown.
+2 -1
View File
@@ -15,7 +15,8 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.149.0"
version = "1.151.2"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+105 -13
View File
@@ -1154,9 +1154,14 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the message with the webxdc instance.
* @param json program-readable data, the actual payload
* @param descr The user-visible description of JSON data,
* in case of a chess game, e.g. the move.
* @param json program-readable data, this is created in JS land as:
* - `payload`: any JS object or primitive.
* - `info`: optional informational message. Will be shown in chat and may be added as system notification.
* note that also users that are not notified explicitly get the `info` or `summary` update shown in the chat.
* - `document`: optional document name. shown eg. in title bar.
* - `summary`: optional summary. shown beside app icon.
* - `notify`: optional array of other users `selfAddr` to be notified e.g. by a sound about `info` or `summary`.
* @param descr Deprecated, set to NULL
* @return 1=success, 0=error
*/
int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const char* json, const char* descr);
@@ -2541,6 +2546,8 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_PROXY with dc_lot_t::text1=address:
* ask the user if they want to use the given proxy.
* if so, call dc_set_config_from_qr() and restart I/O.
* On success, dc_get_config(context, "proxy_url")
* will contain the new proxy in normalized form as the first element.
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* e-mail address scanned, optionally, a draft message could be set in
@@ -2590,13 +2597,15 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* The returned text will also work as a normal https:-link,
* so that the QR code is useful also without Delta Chat being installed
* or can be passed to contacts through other channels.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id If set to a group-chat-id,
@@ -4195,6 +4204,11 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* true if the Webxdc should get full internet access, including Webrtc.
* currently, this is only true for encrypted Webxdc's in the self chat
* that have requested internet access in the manifest.
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
+ Should be exposed to `webxdc.sendUpdateMaxSize` in JS land.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
@@ -4480,6 +4494,7 @@ int dc_msg_is_info (const dc_msg_t* msg);
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
*
* Even when you display an icon,
* you should still display the text of the informational message using dc_msg_get_text()
@@ -4509,6 +4524,24 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
/**
* Get link attached to an webxdc info message.
* The info message needs to be of type DC_INFO_WEBXDC_INFO_MESSAGE.
*
* Typically, this is used to set `document.location.href` in JS land.
*
* Webxdc apps can define the link by setting `update.href` when sending and update,
* see dc_send_webxdc_status_update().
*
* @memberof dc_msg_t
* @param msg The info message object.
* Not: the webxdc instance.
* @return The link to be set to `document.location.href` in JS land.
* Returns NULL if there is no link attached to the info message and on errors.
*/
char* dc_msg_get_webxdc_href (const dc_msg_t* msg);
/**
* Check if a message is still in creation. A message is in creation between
* the calls to dc_prepare_msg() and dc_send_msg().
@@ -5709,8 +5742,14 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_CERTCK_STRICT 1
/**
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.
* Accept certificates that are expired, self-signed
* or not valid for the server hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID 2
/**
* For API compatibility only: Treat this as DC_CERTCK_ACCEPT_INVALID on reading.
* Must not be written.
*/
#define DC_CERTCK_ACCEPT_INVALID_CERTIFICATES 3
@@ -5865,15 +5904,26 @@ int dc_event_get_data2_int(dc_event_t* event);
/**
* Get data associated with an event object.
* The meaning of the data depends on the event ID
* returned as @ref DC_EVENT constants by dc_event_get_id().
* See also dc_event_get_data1_int() and dc_event_get_data2_int().
* The meaning of the data depends on the event ID returned as @ref DC_EVENT constants.
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data2" as a string or NULL.
* the meaning depends on the event type associated with this event.
* Once you're done with the string, you have to unref it using dc_unref_str().
* @return "data1" string or NULL.
* The meaning depends on the event type associated with this event.
* Must be freed using dc_str_unref().
*/
char* dc_event_get_data1_str(dc_event_t* event);
/**
* Get data associated with an event object.
* The meaning of the data depends on the event ID returned as @ref DC_EVENT constants.
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data2" string or NULL.
* The meaning depends on the event type associated with this event.
* Must be freed using dc_str_unref().
*/
char* dc_event_get_data2_str(dc_event_t* event);
@@ -6071,12 +6121,35 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_INCOMING_REACTION 2002
/**
* A webxdc wants an info message or a changed summary to be notified.
*
* @param data1 (int) contact_id ID _and_ (char*) href.
* - dc_event_get_data1_int() returns contact_id of the sending contact.
* - dc_event_get_data1_str() returns the href as set to `update.href`.
* @param data2 (int) msg_id _and_ (char*) text_to_notify.
* - dc_event_get_data2_int() returns the msg_id,
* referring to the webxdc-info-message, if there is any.
* Sometimes no webxdc-info-message is added to the chat
* and yet a notification is sent; in this case the msg_id
* of the webxdc instance is returned.
* - dc_event_get_data2_str() returns text_to_notify,
* the text that shall be shown in the notification.
* string must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_INCOMING_WEBXDC_NOTIFY 2003
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
*
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
*
* If the message is a webxdc info message,
* dc_msg_get_parent() returns the webxdc instance the notification belongs to.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
*/
@@ -6360,6 +6433,25 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
/**
* Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
*
* This event is only emitted by the account manager.
*/
#define DC_EVENT_ACCOUNTS_CHANGED 2302
/**
* Inform that an account property that might be shown in the account list changed, namely:
* - is_configured (see dc_is_configured())
* - displayname
* - selfavatar
* - private_tag
*
* This event is emitted from the account whose property changed.
*/
#define DC_EVENT_ACCOUNTS_ITEM_CHANGED 2303
/**
* Inform that some events have been skipped due to event channel overflow.
+54 -12
View File
@@ -542,6 +542,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingReaction { .. } => 2002,
EventType::IncomingWebxdcNotify { .. } => 2003,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
@@ -568,6 +569,8 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::AccountsBackgroundFetchDone => 2200,
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
EventType::AccountsChanged => 2302,
EventType::AccountsItemChanged => 2303,
EventType::EventChannelOverflow { .. } => 2400,
#[allow(unreachable_patterns)]
#[cfg(test)]
@@ -600,9 +603,12 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ConfigSynced { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone => 0,
EventType::ChatlistChanged => 0,
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged => 0,
EventType::IncomingReaction { contact_id, .. }
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -674,6 +680,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::ChatlistItemChanged { .. }
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::ConfigSynced { .. }
| EventType::ChatModified(_)
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
@@ -681,6 +689,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
| EventType::IncomingWebxdcNotify { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -700,6 +709,27 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_event_get_data1_str(event: *mut dc_event_t) -> *mut libc::c_char {
if event.is_null() {
eprintln!("ignoring careless call to dc_event_get_data1_str()");
return ptr::null_mut();
}
let event = &(*event).typ;
match event {
EventType::IncomingWebxdcNotify { href, .. } => {
if let Some(href) = href {
href.to_c_string().unwrap_or_default().into_raw()
} else {
ptr::null_mut()
}
}
_ => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut libc::c_char {
if event.is_null() {
@@ -748,6 +778,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
@@ -775,6 +807,9 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
.to_c_string()
.unwrap_or_default()
.into_raw(),
EventType::IncomingWebxdcNotify { text, .. } => {
text.to_c_string().unwrap_or_default().into_raw()
}
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -1059,7 +1094,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
msg_id: u32,
json: *const libc::c_char,
descr: *const libc::c_char,
_descr: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_webxdc_status_update()");
@@ -1067,14 +1102,10 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
}
let ctx = &*context;
block_on(ctx.send_webxdc_status_update(
MsgId::new(msg_id),
&to_string_lossy(json),
&to_string_lossy(descr),
))
.context("Failed to send webxdc update")
.log_err(ctx)
.is_ok() as libc::c_int
block_on(ctx.send_webxdc_status_update(MsgId::new(msg_id), &to_string_lossy(json)))
.context("Failed to send webxdc update")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -3681,6 +3712,17 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int
ffi_msg.message.get_info_type() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_webxdc_href()");
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_webxdc_href().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.149.0"
version = "1.151.2"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
+14 -9
View File
@@ -1135,9 +1135,11 @@ impl CommandApi {
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
MessageObject::from_msg_id(&ctx, msg_id)
let message_object = MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))?
.with_context(|| format!("Message {msg_id} does not exist for account {account_id}"))?;
Ok(message_object)
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
@@ -1161,7 +1163,10 @@ impl CommandApi {
messages.insert(
message_id,
match message_result {
Ok(message) => MessageLoadResult::Message(message),
Ok(Some(message)) => MessageLoadResult::Message(message),
Ok(None) => MessageLoadResult::LoadingError {
error: "Message does not exist".to_string(),
},
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
@@ -1762,10 +1767,10 @@ impl CommandApi {
account_id: u32,
instance_msg_id: u32,
update_str: String,
description: String,
_descr: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str)
.await
}
@@ -1999,9 +2004,7 @@ impl CommandApi {
async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result<Option<MessageObject>> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
Ok(Some(
MessageObject::from_msg_id(&ctx, draft.get_id()).await?,
))
Ok(MessageObject::from_msg_id(&ctx, draft.get_id()).await?)
} else {
Ok(None)
}
@@ -2170,7 +2173,9 @@ impl CommandApi {
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id)
.await?
.context("Just sent message does not exist")?;
Ok((msg_id.to_u32(), message))
}
+11 -5
View File
@@ -88,11 +88,17 @@ pub(crate) async fn get_chat_list_item_by_id(
let (last_updated, message_type) = match last_msgid {
Some(id) => {
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
if let Some(last_message) =
deltachat::message::Message::load_from_db_optional(ctx, id).await?
{
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
} else {
// Message may be deleted by the time we try to load it.
(None, None)
}
}
None => (None, None),
};
+36
View File
@@ -106,6 +106,15 @@ pub enum EventType {
reaction: String,
},
/// Incoming webxdc info or summary update, should be notified.
#[serde(rename_all = "camelCase")]
IncomingWebxdcNotify {
contact_id: u32,
msg_id: u32,
text: String,
href: Option<String>,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -277,6 +286,20 @@ pub enum EventType {
#[serde(rename_all = "camelCase")]
ChatlistItemChanged { chat_id: Option<u32> },
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
///
/// This event is only emitted by the account manager
AccountsChanged,
/// Inform that an account property that might be shown in the account list changed, namely:
/// - is_configured (see is_configured())
/// - displayname
/// - selfavatar
/// - private_tag
///
/// This event is emitted from the account whose property changed.
AccountsItemChanged,
/// Inform than some events have been skipped due to event channel overflow.
EventChannelOverflow { n: u64 },
}
@@ -319,6 +342,17 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
reaction: reaction.as_str().to_string(),
},
CoreEventType::IncomingWebxdcNotify {
contact_id,
msg_id,
text,
href,
} => IncomingWebxdcNotify {
contact_id: contact_id.to_u32(),
msg_id: msg_id.to_u32(),
text,
href,
},
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
@@ -409,6 +443,8 @@ impl From<CoreEventType> for EventType {
},
CoreEventType::ChatlistChanged => ChatlistChanged,
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
CoreEventType::AccountsChanged => AccountsChanged,
CoreEventType::AccountsItemChanged => AccountsItemChanged,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
+7 -4
View File
@@ -112,8 +112,10 @@ enum MessageQuote {
}
impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Option<Self>> {
let Some(message) = Message::load_from_db_optional(context, msg_id).await? else {
return Ok(None);
};
let sender_contact = Contact::get_by_id(context, message.get_from_id())
.await
@@ -183,7 +185,7 @@ impl MessageObject {
.map(Into::into)
.collect();
Ok(MessageObject {
let message_object = MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
@@ -244,7 +246,8 @@ impl MessageObject {
reactions,
vcard_contact: vcard_contacts.first().cloned(),
})
};
Ok(Some(message_object))
}
}
+84 -11
View File
@@ -6,87 +6,161 @@ use typescript_type_def::TypeDef;
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "kind")]
pub enum QrObject {
/// Ask the user whether to verify the contact.
///
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
AskVerifyContact {
/// ID of the contact.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user whether to join the group.
AskVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// ID of the contact.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
FprOk {
/// Contact ID.
contact_id: u32,
},
/// Scanned fingerprint does not match the last seen fingerprint.
FprMismatch {
/// Contact ID.
contact_id: Option<u32>,
},
/// The scanned QR code contains a fingerprint but no e-mail address.
FprWithoutAddr {
/// Key fingerprint.
fingerprint: String,
},
/// Ask the user if they want to create an account on the given domain.
Account {
/// Server domain name.
domain: String,
},
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Authentication token.
auth_token: String,
/// Iroh node address.
node_addr: String,
},
/// Ask the user if they want to use the given service for video chats.
WebrtcInstance {
domain: String,
instance_pattern: String,
},
/// Ask the user if they want to use the given proxy.
///
/// Note that HTTP(S) URLs without a path
/// and query parameters are treated as HTTP(S) proxy URL.
/// UI may want to still offer to open the URL
/// in the browser if QR code contents
/// starts with `http://` or `https://`
/// and the QR code was not scanned from
/// the proxy configuration screen.
Proxy {
/// Proxy URL.
///
/// This is the URL that is going to be added.
url: String,
/// Host extracted from the URL to display in the UI.
host: String,
/// Port extracted from the URL to display in the UI.
port: u16,
},
/// Contact address is scanned.
///
/// Optionally, a draft message could be provided.
/// Ask the user if they want to start chatting.
Addr {
/// Contact ID.
contact_id: u32,
/// Draft message.
draft: Option<String>,
},
Url {
url: String,
},
Text {
text: String,
},
/// URL scanned.
///
/// Ask the user if they want to open a browser or copy the URL to clipboard.
Url { url: String },
/// Text scanned.
///
/// Ask the user if they want to copy the text to clipboard.
Text { text: String },
/// Ask the user if they want to withdraw their own QR code.
WithdrawVerifyContact {
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to withdraw their own group invite QR code.
WithdrawVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own QR code.
ReviveVerifyContact {
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own group invite QR code.
ReviveVerifyGroup {
/// Contact ID.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
Login {
address: String,
},
/// `dclogin:` scheme parameters.
///
/// Ask the user if they want to login with the email address.
Login { address: String },
}
impl From<Qr> for QrObject {
@@ -141,7 +215,6 @@ impl From<Qr> for QrObject {
auth_token,
} => QrObject::Backup2 {
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
auth_token,
},
Qr::WebrtcInstance {
+14
View File
@@ -35,6 +35,14 @@ pub struct WebxdcMessageInfo {
source_code_url: Option<String>,
/// True if full internet access should be granted to the app.
internet_access: bool,
/// Address to be used for `window.webxdc.selfAddr` in JS land.
self_addr: String,
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
/// Should be exposed to `window.sendUpdateInterval` in JS land.
send_update_interval: usize,
/// Maximum number of bytes accepted for a serialized update object.
/// Should be exposed to `window.sendUpdateMaxSize` in JS land.
send_update_max_size: usize,
}
impl WebxdcMessageInfo {
@@ -50,6 +58,9 @@ impl WebxdcMessageInfo {
summary,
source_code_url,
internet_access,
self_addr,
send_update_interval,
send_update_max_size,
} = message.get_webxdc_info(context).await?;
Ok(Self {
@@ -59,6 +70,9 @@ impl WebxdcMessageInfo {
summary: maybe_empty_string_to_option(summary),
source_code_url: maybe_empty_string_to_option(source_code_url),
internet_access,
self_addr,
send_update_interval,
send_update_max_size,
})
}
}
+2
View File
@@ -1,4 +1,6 @@
#![recursion_limit = "256"]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
pub mod api;
pub use yerpc;
+1 -1
View File
@@ -58,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.149.0"
"version": "1.151.2"
}
+6
View File
@@ -90,6 +90,11 @@ impl Ratelimit {
pub fn until_can_send(&self) -> Duration {
self.until_can_send_at(SystemTime::now())
}
/// Returns minimum possible update interval in milliseconds.
pub fn update_interval(&self) -> usize {
(self.window.as_millis() as f64 / self.quota) as usize
}
}
#[cfg(test)]
@@ -102,6 +107,7 @@ mod tests {
let mut ratelimit = Ratelimit::new_at(Duration::new(60, 0), 3.0, now);
assert!(ratelimit.can_send_at(now));
assert_eq!(ratelimit.update_interval(), 20_000);
// Send burst of 3 messages.
ratelimit.send_at(now);
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.149.0"
version = "1.151.2"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
+1 -3
View File
@@ -969,9 +969,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"Arguments <msg-id> <json status update> expected"
);
let msg_id = MsgId::new(arg1.parse()?);
context
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
.await?;
context.send_webxdc_status_update(msg_id, arg2).await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
+2 -1
View File
@@ -25,7 +25,8 @@ $ pip install .
## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
2. Install tox `pip install -U tox`
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
+1 -4
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.149.0"
version = "1.151.2"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -24,9 +24,6 @@ classifiers = [
"Topic :: Communications :: Email"
]
readme = "README.md"
dependencies = [
"imap-tools",
]
[tool.setuptools.package-data]
deltachat_rpc_client = [
@@ -61,6 +61,8 @@ class EventType(str, Enum):
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
ACCOUNTS_CHANGED = "AccountsChanged"
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
@@ -1,7 +1,3 @@
"""
Internal Python-level IMAP handling used by the tests.
"""
from __future__ import annotations
import imaplib
@@ -11,17 +7,11 @@ import ssl
from contextlib import contextmanager
from typing import TYPE_CHECKING
from imap_tools import (
AND,
Header,
MailBox,
MailMessage,
MailMessageFlags,
errors,
)
import pytest
from imap_tools import AND, Header, MailBox, MailMessage, MailMessageFlags, errors
if TYPE_CHECKING:
from . import Account
from deltachat_rpc_client import Account
FLAGS = b"FLAGS"
FETCH = b"FETCH"
@@ -29,6 +19,8 @@ ALL = "1:*"
class DirectImap:
"""Internal Python-level IMAP handling."""
def __init__(self, account: Account) -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
@@ -212,3 +204,8 @@ class IdleManager:
def done(self):
"""send idle-done to server if we are currently in idle mode."""
return self.direct_imap.conn.idle.stop()
@pytest.fixture
def direct_imap():
return DirectImap
@@ -0,0 +1,29 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from deltachat_rpc_client import EventType
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import ACFactory
def test_event_on_configuration(acfactory: ACFactory) -> None:
"""
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
"""
account = acfactory.new_preconfigured_account()
account.clear_all_events()
assert not account.is_configured()
future = account.configure.future()
while True:
event = account.wait_for_event()
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
break
assert account.is_configured()
future()
# other tests are written in rust: src/tests/account_events.rs
+2 -3
View File
@@ -12,7 +12,6 @@ import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.direct_imap import DirectImap
from deltachat_rpc_client.rpc import JsonRpcError
@@ -536,7 +535,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_reactions_for_a_reordering_move(acfactory):
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
@@ -560,7 +559,7 @@ def test_reactions_for_a_reordering_move(acfactory):
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = DirectImap(ac2)
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")
@@ -24,6 +24,9 @@ def test_webxdc(acfactory) -> None:
"name": "Chess Board",
"sourceCodeUrl": None,
"summary": None,
"selfAddr": webxdc_info["selfAddr"],
"sendUpdateInterval": 1000,
"sendUpdateMaxSize": 18874368,
}
status_updates = message.get_webxdc_status_updates()
+1
View File
@@ -16,6 +16,7 @@ deps =
pytest
pytest-timeout
pytest-xdist
imap-tools
[testenv:lint]
skipsdist = True
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.149.0"
version = "1.151.2"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "1.149.0"
"version": "1.151.2"
}
+9
View File
@@ -15,6 +15,9 @@ ignore = [
# Unmaintained proc-macro-error
# <https://rustsec.org/advisories/RUSTSEC-2024-0370>
"RUSTSEC-2024-0370",
# Unmaintained instant
"RUSTSEC-2024-0384",
]
[bans]
@@ -30,10 +33,14 @@ skip = [
{ name = "event-listener", version = "2.5.3" },
{ name = "event-listener", version = "4.0.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "fiat-crypto", version = "0.1.20" },
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "http", version = "0.2.12" },
{ name = "nix", version = "0.26.4" },
{ name = "num_enum_derive", version = "0.5.11" },
{ name = "num_enum", version = "0.5.11" },
{ name = "proc-macro-crate", version = "1.3.1" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
@@ -44,6 +51,7 @@ skip = [
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "toml_edit", version = "0.19.15" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
{ name = "windows_aarch64_msvc", version = "<0.52" },
@@ -56,6 +64,7 @@ skip = [
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
{ name = "winreg", version = "0.50.0" },
]
Generated
+30 -31
View File
@@ -7,11 +7,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1729628358,
"narHash": "sha256-2HDSc6BL+bE3S1l3Gn0Z8wWvvfBEUEjvXkNIQ11Aifk=",
"lastModified": 1731356359,
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "52b9e0c0f9cff887d2bb4932f8be4e062ba0802d",
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1714112748,
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
"lastModified": 1731393059,
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
"owner": "nix-community",
"repo": "fenix",
"rev": "3ae4b908a795b6a3824d401a0702e11a7157d7e1",
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
"type": "github"
},
"original": {
@@ -83,11 +83,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
@@ -101,11 +101,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"lastModified": 1721727458,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
"owner": "nix-community",
"repo": "naersk",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
"type": "github"
},
"original": {
@@ -116,11 +116,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"lastModified": 1730207686,
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4",
"type": "github"
},
"original": {
@@ -131,11 +131,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1729413321,
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1713895582,
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
@@ -163,10 +163,9 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1711668574,
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
"lastModified": 0,
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
"type": "path"
},
"original": {
@@ -176,11 +175,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1729413321,
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
@@ -203,11 +202,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1714031783,
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
"lastModified": 1731342671,
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
"type": "github"
},
"original": {
+27 -23
View File
@@ -363,6 +363,8 @@
mkRustPackages "x86_64-linux" //
mkRustPackages "armv7l-linux" //
mkRustPackages "armv6l-linux" //
mkRustPackages "x86_64-darwin" //
mkRustPackages "aarch64-darwin" //
mkAndroidPackages "armeabi-v7a" //
mkAndroidPackages "arm64-v8a" //
mkAndroidPackages "x86" //
@@ -479,8 +481,8 @@
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat_rpc_server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-client =
@@ -533,28 +535,30 @@
};
};
devShells.default = let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in pkgs.mkShell {
devShells.default =
let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in
pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
};
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
};
}
);
}
+2
View File
@@ -8,6 +8,8 @@
//! is assumed to be set to "no".
//!
//! For received messages, DelSp parameter is honoured.
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
/// Wraps line to 72 characters using format=flowed soft breaks.
///
+907 -1546
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1,6 +1,7 @@
// Generated!
module.exports = {
DC_CERTCK_ACCEPT_INVALID: 2,
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
DC_CERTCK_AUTO: 0,
DC_CERTCK_STRICT: 1,
@@ -30,6 +31,8 @@ module.exports = {
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_DOWNLOAD_UNDECIPHERABLE: 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
DC_EVENT_ACCOUNTS_CHANGED: 2302,
DC_EVENT_ACCOUNTS_ITEM_CHANGED: 2303,
DC_EVENT_CHANNEL_OVERFLOW: 2400,
DC_EVENT_CHATLIST_CHANGED: 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
@@ -51,6 +54,7 @@ module.exports = {
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
DC_EVENT_INCOMING_REACTION: 2002,
DC_EVENT_INCOMING_WEBXDC_NOTIFY: 2003,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
+3
View File
@@ -17,6 +17,7 @@ module.exports = {
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
@@ -43,5 +44,7 @@ module.exports = {
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2302: 'DC_EVENT_ACCOUNTS_CHANGED',
2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
}
+7
View File
@@ -1,6 +1,7 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID = 2,
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
@@ -30,6 +31,8 @@ export enum C {
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_DOWNLOAD_UNDECIPHERABLE = 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
DC_EVENT_ACCOUNTS_CHANGED = 2302,
DC_EVENT_ACCOUNTS_ITEM_CHANGED = 2303,
DC_EVENT_CHANNEL_OVERFLOW = 2400,
DC_EVENT_CHATLIST_CHANGED = 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
@@ -51,6 +54,7 @@ export enum C {
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
DC_EVENT_INCOMING_REACTION = 2002,
DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
@@ -324,6 +328,7 @@ export const EventId2EventName: { [key: number]: string } = {
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
@@ -350,5 +355,7 @@ export const EventId2EventName: { [key: number]: string } = {
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2302: 'DC_EVENT_ACCOUNTS_CHANGED',
2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
}
+1 -1
View File
@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.149.0"
"version": "1.151.2"
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.149.0"
version = "1.151.2"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"
+1 -1
View File
@@ -705,7 +705,7 @@ class TestOfflineChat:
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
qr = ac1.get_setup_contact_qr()
assert qr.startswith("OPENPGP4FPR:")
assert qr.startswith("https://i.delta.chat")
res = ac2.check_qr(qr)
assert res.is_ask_verifycontact()
assert not res.is_ask_verifygroup()
+1 -1
View File
@@ -1 +1 @@
2024-11-05
2024-11-26
+2 -2
View File
@@ -25,7 +25,7 @@ def build_source_package(version, filename):
def pack(name, contents):
contents = contents.encode()
tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}")
tar_info = tarfile.TarInfo(f"deltachat_rpc_server-{version}/{name}")
tar_info.mode = 0o644
tar_info.size = len(contents)
pkg.addfile(tar_info, BytesIO(contents))
@@ -167,7 +167,7 @@ def main():
cargo_manifest = tomllib.load(fp)
version = cargo_manifest["package"]["version"]
if sys.argv[1] == "source":
filename = f"deltachat-rpc-server-{version}.tar.gz"
filename = f"deltachat_rpc_server-{version}.tar.gz"
build_source_package(version, filename)
else:
arch = sys.argv[1]
+3
View File
@@ -139,6 +139,7 @@ impl Accounts {
ctx.open("".to_string()).await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
Ok(account_config.id)
}
@@ -156,6 +157,7 @@ impl Accounts {
.build()
.await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
Ok(account_config.id)
}
@@ -190,6 +192,7 @@ impl Accounts {
.context("failed to remove account data")?;
}
self.config.remove_account(id).await?;
self.emit_event(EventType::AccountsChanged);
Ok(())
}
-1
View File
@@ -260,7 +260,6 @@ fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str>
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use tokio::fs;
use tokio::io::AsyncReadExt;
+47 -37
View File
@@ -765,27 +765,19 @@ impl ChatId {
);
let chat = Chat::load_from_db(context, self).await?;
context
.sql
.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
(self,),
)
.await?;
context
.sql
.execute("DELETE FROM msgs WHERE chat_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM chats WHERE id=?;", (self,))
.transaction(|transaction| {
transaction.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
(self,),
)?;
transaction.execute("DELETE FROM msgs WHERE chat_id=?", (self,))?;
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (self,))?;
transaction.execute("DELETE FROM chats WHERE id=?", (self,))?;
Ok(())
})
.await?;
context.emit_msgs_changed_without_ids();
@@ -922,24 +914,27 @@ impl ChatId {
&& old_draft.chat_id == self
&& old_draft.state == MessageState::OutDraft
{
context
.sql
.execute(
"UPDATE msgs
SET timestamp=?,type=?,txt=?,txt_normalized=?,param=?,mime_in_reply_to=?
WHERE id=?;",
(
time(),
msg.viewtype,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
),
)
.await?;
return Ok(true);
let affected_rows = context
.sql.execute(
"UPDATE msgs
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
WHERE id=?7
AND (type <> ?2
OR txt <> ?3
OR txt_normalized <> ?4
OR param <> ?5
OR mime_in_reply_to <> ?6);",
(
time(),
msg.viewtype,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
),
).await?;
return Ok(affected_rows > 0);
}
}
}
@@ -5371,7 +5366,7 @@ mod tests {
// Eventually, first removal message arrives.
// This has no effect.
bob.recv_msg(&remove1).await;
bob.recv_msg_trash(&remove1).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())
}
@@ -7696,4 +7691,19 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_do_not_overwrite_draft() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let mut msg = Message::new_text("This is a draft message".to_string());
let self_chat = alice.get_self_chat().await.id;
self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap();
let draft1 = self_chat.get_draft(&alice).await?.unwrap();
SystemTime::shift(Duration::from_secs(1));
self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap();
let draft2 = self_chat.get_draft(&alice).await?.unwrap();
assert_eq!(draft1.timestamp_sort, draft2.timestamp_sort);
Ok(())
}
}
+33 -7
View File
@@ -394,25 +394,32 @@ impl Chatlist {
&chat_loaded
};
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id)
let lastmsg = if let Some(lastmsg_id) = lastmsg_id {
// Message may be deleted by the time we try to load it,
// so use `load_from_db_optional` instead of `load_from_db`.
Message::load_from_db_optional(context, lastmsg_id)
.await
.context("loading message failed")?;
.context("Loading message failed")?
} else {
None
};
let lastcontact = if let Some(lastmsg) = &lastmsg {
if lastmsg.from_id == ContactId::SELF {
(Some(lastmsg), None)
None
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
(Some(lastmsg), Some(lastcontact))
Some(lastcontact)
}
Chattype::Single => (Some(lastmsg), None),
Chattype::Single => None,
}
}
} else {
(None, None)
None
};
if chat.id.is_archived_link() {
@@ -761,6 +768,25 @@ mod tests {
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
}
/// Tests that summary does not fail to load
/// if the draft was deleted after loading the chatlist.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_deleted_draft() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let mut msg = Message::new_text("Foobar".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
chat_id.set_draft(&t, None).await.unwrap();
let summary_res = chats.get_summary(&t, 0, None).await;
assert!(summary_res.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;
+6
View File
@@ -791,6 +791,12 @@ impl Context {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
}
if matches!(
key,
Config::Displayname | Config::Selfavatar | Config::PrivateTag
) {
self.emit_event(EventType::AccountsItemChanged);
}
if key.is_synced() {
self.emit_event(EventType::ConfigSynced { key });
}
+2 -3
View File
@@ -36,10 +36,10 @@ use crate::message::Message;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::time;
use crate::{chat, e2ee, provider};
use crate::{stock_str, EventType};
use deltachat_contact_tools::addr_cmp;
macro_rules! progress {
@@ -486,6 +486,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
update_device_chats_handle.await??;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.emit_event(EventType::AccountsItemChanged);
Ok(configured_param)
}
@@ -611,8 +612,6 @@ pub enum Error {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::login_param::EnteredServerLoginParam;
+7 -13
View File
@@ -67,19 +67,15 @@ fn parse_server<B: BufRead>(
let typ = server_event
.attributes()
.find(|attr| {
attr.as_ref()
.map(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.to_lowercase()
== "type"
})
.unwrap_or_default()
.find_map(|attr| {
attr.ok().filter(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.eq_ignore_ascii_case("type")
})
})
.map(|typ| {
typ.unwrap()
.decode_and_unescape_value(reader.decoder())
typ.decode_and_unescape_value(reader.decoder())
.unwrap_or_default()
.to_lowercase()
})
@@ -272,8 +268,6 @@ pub(crate) async fn moz_autoconfigure(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
-2
View File
@@ -215,8 +215,6 @@ pub(crate) async fn outlk_autodiscover(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
+3 -3
View File
@@ -1252,15 +1252,15 @@ impl Contact {
let fingerprint_self = load_self_public_key(context)
.await?
.fingerprint()
.dc_fingerprint()
.to_string();
let fingerprint_other_verified = peerstate
.peek_key(true)
.map(|k| k.fingerprint().to_string())
.map(|k| k.dc_fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(false)
.map(|k| k.fingerprint().to_string())
.map(|k| k.dc_fingerprint().to_string())
.unwrap_or_default();
if addr < peerstate.addr {
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
+50 -4
View File
@@ -10,6 +10,7 @@ use std::time::Duration;
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use pgp::types::PublicKeyTrait;
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, OnceCell, RwLock};
@@ -275,7 +276,7 @@ pub struct InnerContext {
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: std::sync::RwLock<String>,
pub(crate) last_error: parking_lot::RwLock<String>,
/// If debug logging is enabled, this contains all necessary information
///
@@ -445,7 +446,7 @@ impl Context {
metadata: RwLock::new(None),
creation_time: tools::Time::now(),
last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()),
last_error: parking_lot::RwLock::new("".to_string()),
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
@@ -471,6 +472,14 @@ impl Context {
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
// The next line is mainly for iOS:
// iOS starts a separate process for receiving notifications and if the user concurrently
// starts the app, the UI process opens the database but waits with calling start_io()
// until the notifications process finishes.
// Now, some configs may have changed, so, we need to invalidate the cache.
self.sql.config_cache.write().await.clear();
self.scheduler.start(self.clone()).await;
}
@@ -781,7 +790,7 @@ impl Context {
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
let fingerprint_str = match load_self_public_key(self).await {
Ok(key) => key.fingerprint().hex(),
Ok(key) => key.dc_fingerprint().hex(),
Err(err) => format!("<key failure: {err}>"),
};
@@ -1177,7 +1186,7 @@ impl Context {
EncryptPreference::Mutual,
&public_key,
);
let fingerprint = public_key.fingerprint();
let fingerprint = public_key.dc_fingerprint();
peerstate.set_verified(public_key, fingerprint, "".to_string())?;
peerstate.save_to_db(&self.sql).await?;
chat_id
@@ -2063,4 +2072,41 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
let alice = TestContext::new_alice().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Change the config circumventing the cache
// This simulates what the notification plugin on iOS might do
// because it runs in a different process
alice
.sql
.execute(
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
(),
)
.await?;
// Alice's Delta Chat doesn't know about it yet:
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Starting IO will fail of course because no server settings are configured,
// but it should invalidate the caches:
alice.start_io().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("0".to_string())
);
Ok(())
}
}
+2
View File
@@ -60,9 +60,11 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
"time": time,
}),
info: None,
href: None,
summary: None,
document: None,
uid: None,
notify: None,
},
time,
)
+1 -1
View File
@@ -182,7 +182,7 @@ pub(crate) async fn get_autocrypt_peerstate(
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
&header.public_key.dc_fingerprint(),
from,
)
.await?;
+1 -1
View File
@@ -440,7 +440,7 @@ mod tests {
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
alice
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#, "d")
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#)
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
+3 -3
View File
@@ -303,12 +303,12 @@ Sent with my Delta Chat Messenger: https://delta.chat";
last_seen_autocrypt: 14,
prefer_encrypt,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 15,
gossip_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
+29
View File
@@ -107,6 +107,21 @@ pub enum EventType {
reaction: Reaction,
},
/// A webxdc wants an info message or a changed summary to be notified.
IncomingWebxdcNotify {
/// ID of the contact sending.
contact_id: ContactId,
/// ID of the added info message or webxdc instance in case of summary change.
msg_id: MsgId,
/// Text to notify.
text: String,
/// Link assigned to this notification, if any.
href: Option<String>,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -332,6 +347,20 @@ pub enum EventType {
chat_id: Option<ChatId>,
},
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
///
/// This event is only emitted by the account manager
AccountsChanged,
/// Inform that an account property that might be shown in the account list changed, namely:
/// - is_configured (see [crate::context::Context::is_configured])
/// - displayname
/// - selfavatar
/// - private_tag
///
/// This event is emitted from the account whose property changed.
AccountsItemChanged,
/// Event for using in tests, e.g. as a fence between normally generated events.
#[cfg(test)]
Test,
+4 -3
View File
@@ -4,7 +4,7 @@ use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use ::pgp::types::KeyTrait;
use ::pgp::types::PublicKeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use futures::TryStreamExt;
use futures_lite::FutureExt;
@@ -426,6 +426,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
if res.is_ok() {
context.emit_event(EventType::ImexProgress(999));
res = context.sql.run_migrations(context).await;
context.emit_event(EventType::AccountsItemChanged);
}
if res.is_ok() {
delete_and_reset_all_device_msgs(context)
@@ -750,7 +751,7 @@ where
true => "private",
};
let id = id.map_or("default".into(), |i| i.to_string());
let fp = DcKey::fingerprint(key).hex();
let fp = key.dc_fingerprint().hex();
format!("{kind}-key-{addr}-{id}-{fp}.asc")
};
let path = dir.join(&file_name);
@@ -878,7 +879,7 @@ mod tests {
.unwrap()
.strip_suffix(".asc")
.unwrap();
assert_eq!(fingerprint, DcKey::fingerprint(&key).hex());
assert_eq!(fingerprint, key.dc_fingerprint().hex());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{blobdir}/{filename}");
let bytes = tokio::fs::read(&filename).await.unwrap();
+13 -5
View File
@@ -11,7 +11,8 @@ use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use pgp::types::{PublicKeyTrait, SecretKeyTrait};
use rand::thread_rng;
use tokio::runtime::Handle;
use crate::config::Config;
@@ -26,7 +27,7 @@ use crate::tools::{self, time_elapsed};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
@@ -93,8 +94,8 @@ pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
/// The fingerprint for the key.
fn fingerprint(&self) -> Fingerprint {
Fingerprint::new(KeyTrait::fingerprint(self))
fn dc_fingerprint(&self) -> Fingerprint {
PublicKeyTrait::fingerprint(self).into()
}
fn is_private() -> bool;
@@ -233,7 +234,8 @@ impl DcSecretKey for SignedSecretKey {
fn split_public_key(&self) -> Result<SignedPublicKey> {
self.verify()?;
let unsigned_pubkey = SecretKeyTrait::public_key(self);
let signed_pubkey = unsigned_pubkey.sign(self, || "".into())?;
let mut rng = thread_rng();
let signed_pubkey = unsigned_pubkey.sign(&mut rng, self, || "".into())?;
Ok(signed_pubkey)
}
}
@@ -395,6 +397,12 @@ impl Fingerprint {
}
}
impl From<pgp::types::Fingerprint> for Fingerprint {
fn from(fingerprint: pgp::types::Fingerprint) -> Fingerprint {
Self::new(fingerprint.as_bytes().into())
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")
+2 -1
View File
@@ -16,7 +16,8 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
+6 -12
View File
@@ -244,18 +244,14 @@ impl Kml {
self.tag = KmlTag::PlacemarkPoint;
} else if tag == "coordinates" && self.tag == KmlTag::PlacemarkPoint {
self.tag = KmlTag::PlacemarkPointCoordinates;
if let Some(acc) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.to_lowercase()
== "accuracy"
})
.unwrap_or_default()
if let Some(acc) = event.attributes().find_map(|attr| {
attr.ok().filter(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.eq_ignore_ascii_case("accuracy")
})
}) {
let v = acc
.unwrap()
.decode_and_unescape_value(reader.decoder())
.unwrap_or_default();
@@ -880,8 +876,6 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::message::MessageState;
+2 -2
View File
@@ -50,13 +50,13 @@ impl Context {
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
pub fn set_last_error(&self, error: &str) {
let mut last_error = self.last_error.write().unwrap();
let mut last_error = self.last_error.write();
*last_error = error.to_string();
}
/// Get last error string.
pub fn get_last_error(&self) -> String {
let last_error = &*self.last_error.read().unwrap();
let last_error = &*self.last_error.read();
last_error.clone()
}
}
+116 -1
View File
@@ -1693,6 +1693,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
m.id AS id,
m.chat_id AS chat_id,
m.state AS state,
m.download_state as download_state,
m.ephemeral_timer AS ephemeral_timer,
m.param AS param,
m.from_id AS from_id,
@@ -1708,6 +1709,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let state: MessageState = row.get("state")?;
let download_state: DownloadState = row.get("download_state")?;
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
@@ -1719,6 +1721,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
id,
chat_id,
state,
download_state,
param,
from_id,
rfc724_mid,
@@ -1748,6 +1751,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
id,
curr_chat_id,
curr_state,
curr_download_state,
curr_param,
curr_from_id,
curr_rfc724_mid,
@@ -1757,7 +1761,14 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
_curr_ephemeral_timer,
) in msgs
{
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
if curr_download_state != DownloadState::Done {
if curr_state == MessageState::InFresh {
// Don't mark partially downloaded messages as seen or send a read receipt since
// they are not really seen by the user.
update_msg_state(context, id, MessageState::InNoticed).await?;
updated_chat_ids.insert(curr_chat_id);
}
} else if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
update_msg_state(context, id, MessageState::InSeen).await?;
info!(context, "Seen message {}.", id);
@@ -2583,6 +2594,110 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_markseen_not_downloaded_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
.await?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert_eq!(msg.state, MessageState::InFresh);
markseen_msgs(alice, vec![msg.id]).await?;
// A not downloaded message can be seen only if it's seen on another device.
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
// Marking the message as seen again is a no op.
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::InProgress)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::Failure)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::Undecipherable)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
assert!(
!alice
.sql
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
.await?
);
alice.set_config(Config::DownloadLimit, None).await?;
// Let's assume that Alice and Bob resolved the problem with encryption.
let old_msg = msg;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, old_msg.chat_id);
assert_eq!(msg.download_state, DownloadState::Done);
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert!(msg.get_showpadlock());
// The message state mustn't be downgraded to `InFresh`.
assert_eq!(msg.state, MessageState::InNoticed);
markseen_msgs(alice, vec![msg.id]).await?;
let msg = Message::load_from_db(alice, msg.id).await?;
assert_eq!(msg.state, MessageState::InSeen);
assert_eq!(
alice
.sql
.count("SELECT COUNT(*) FROM smtp_mdns", ())
.await?,
1
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
.await?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(msg.state, MessageState::InFresh);
alice.set_config(Config::DownloadLimit, None).await?;
let seen = true;
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
.await
.unwrap()
.unwrap();
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
.await
.unwrap();
assert_eq!(msg.download_state, DownloadState::Done);
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert!(msg.get_showpadlock());
assert_eq!(msg.state, MessageState::InSeen);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_state() -> Result<()> {
let alice = TestContext::new_alice().await;
+15 -11
View File
@@ -6,7 +6,6 @@ use anyhow::{bail, Context as _, Result};
use base64::Engine as _;
use chrono::TimeZone;
use email::Mailbox;
use format_flowed::{format_flowed, format_flowed_quote};
use lettre_email::{Address, Header, MimeMultipartType, PartBuilder};
use tokio::fs;
@@ -1300,9 +1299,18 @@ impl MimeFactory {
let final_text = placeholdertext.as_deref().unwrap_or(&msg.text);
let mut quoted_text = msg
.quoted_text()
.map(|quote| format_flowed_quote(&quote) + "\r\n\r\n");
let mut quoted_text = None;
if let Some(msg_quoted_text) = msg.quoted_text() {
let mut some_quoted_text = String::new();
for quoted_line in msg_quoted_text.split('\n') {
some_quoted_text += "> ";
some_quoted_text += quoted_line;
some_quoted_text += "\r\n";
}
some_quoted_text += "\r\n";
quoted_text = Some(some_quoted_text)
}
if !is_encrypted && msg.param.get_bool(Param::ProtectQuote).unwrap_or_default() {
// Message is not encrypted but quotes encrypted message.
quoted_text = Some("> ...\r\n\r\n".to_string());
@@ -1312,7 +1320,6 @@ impl MimeFactory {
// Delta Chat.
quoted_text = Some("\r\n".to_string());
}
let flowed_text = format_flowed(final_text);
let is_reaction = msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
@@ -1322,7 +1329,7 @@ impl MimeFactory {
"{}{}{}{}{}{}",
fwdhint.unwrap_or_default(),
quoted_text.unwrap_or_default(),
escape_message_footer_marks(&flowed_text),
escape_message_footer_marks(final_text),
if !final_text.is_empty() && !footer.is_empty() {
"\r\n\r\n"
} else {
@@ -1332,10 +1339,8 @@ impl MimeFactory {
footer
);
let mut main_part = PartBuilder::new().header((
"Content-Type",
"text/plain; charset=utf-8; format=flowed; delsp=no",
));
let mut main_part =
PartBuilder::new().header(("Content-Type", "text/plain; charset=utf-8"));
main_part = self.add_message_text(main_part, message_text);
if is_reaction {
@@ -2274,7 +2279,6 @@ mod tests {
let msg = message.as_string();
let header_end = msg.find("Hi").unwrap();
#[allow(clippy::indexing_slicing)]
let headers = msg[0..header_end].trim();
assert!(!headers.lines().any(|l| l.trim().is_empty()));
+32 -33
View File
@@ -665,11 +665,13 @@ impl MimeMessage {
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
/// containing a description. If such a message is detected, text from the first part can be
/// moved to the second part, and the first part dropped.
#[allow(clippy::indexing_slicing)]
fn squash_attachment_parts(&mut self) {
if let [textpart, filepart] = &self.parts[..] {
let need_drop = textpart.typ == Viewtype::Text
&& match filepart.typ {
if self.parts.len() == 2
&& self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
&& self
.parts
.get(1)
.map_or(false, |filepart| match filepart.typ {
Viewtype::Image
| Viewtype::Gif
| Viewtype::Sticker
@@ -680,24 +682,24 @@ impl MimeMessage {
| Viewtype::File
| Viewtype::Webxdc => true,
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
};
})
{
let mut parts = std::mem::take(&mut self.parts);
let Some(mut filepart) = parts.pop() else {
// Should never happen.
return;
};
let Some(textpart) = parts.pop() else {
// Should never happen.
return;
};
if need_drop {
let mut filepart = self.parts.swap_remove(1);
// insert new one
filepart.msg.clone_from(&self.parts[0].msg);
if let Some(quote) = self.parts[0].param.get(Param::Quote) {
filepart.param.set(Param::Quote, quote);
}
// forget the one we use now
self.parts[0].msg = "".to_string();
// swap new with old
self.parts.push(filepart); // push to the end
let _ = self.parts.swap_remove(0); // drops first element, replacing it with the last one in O(1)
filepart.msg.clone_from(&textpart.msg);
if let Some(quote) = textpart.param.get(Param::Quote) {
filepart.param.set(Param::Quote, quote);
}
self.parts = vec![filepart];
}
}
@@ -1665,10 +1667,11 @@ impl MimeMessage {
{
let mut to_list =
get_all_addresses_from_header(&report.headers, "x-failed-recipients");
let to = if to_list.len() == 1 {
Some(to_list.pop().unwrap())
let to = if to_list.len() != 1 {
// We do not know which recipient failed
None
} else {
None // We do not know which recipient failed
to_list.pop()
};
return Ok(Some(DeliveryReport {
@@ -1751,7 +1754,6 @@ impl MimeMessage {
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
/// Also you should add a test in receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) {
let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
@@ -1914,18 +1916,17 @@ pub(crate) struct DeliveryReport {
pub failure: bool,
}
#[allow(clippy::indexing_slicing)]
pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
let mut msgids = Vec::new();
for id in ids.split_whitespace() {
let mut id = id.to_string();
if id.starts_with('<') {
id = id[1..].to_string();
}
if id.ends_with('>') {
id = id[..id.len() - 1].to_string();
}
if let Some(id_without_prefix) = id.strip_prefix('<') {
id = id_without_prefix.to_string();
};
if let Some(id_without_suffix) = id.strip_suffix('>') {
id = id_without_suffix.to_string();
};
if !id.is_empty() {
msgids.push(id);
}
@@ -2362,8 +2363,6 @@ async fn ndn_maybe_add_info_msg(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use mailparse::ParsedMail;
use super::*;
+2 -2
View File
@@ -5,13 +5,13 @@ use std::pin::Pin;
use std::time::Duration;
use anyhow::{format_err, Context as _, Result};
use async_native_tls::TlsStream;
use tokio::net::TcpStream;
use tokio::task::JoinSet;
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::net::session::SessionStream;
use crate::sql::Sql;
use crate::tools::time;
@@ -128,7 +128,7 @@ pub(crate) async fn connect_tls_inner(
host: &str,
strict_tls: bool,
alpn: &[&str],
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
) -> Result<impl SessionStream> {
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
Ok(tls_stream)
+22 -28
View File
@@ -2,45 +2,39 @@
use std::sync::Arc;
use anyhow::Result;
use async_native_tls::{Certificate, Protocol, TlsConnector, TlsStream};
use once_cell::sync::Lazy;
use tokio::io::{AsyncRead, AsyncWrite};
// this certificate is missing on older android devices (eg. lg with android6 from 2017)
// certificate downloaded from https://letsencrypt.org/certificates/
static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
use crate::net::session::SessionStream;
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
pub async fn wrap_tls(
strict_tls: bool,
hostname: &str,
alpn: &[&str],
stream: T,
) -> Result<TlsStream<T>> {
let tls_builder = TlsConnector::new()
.min_protocol_version(Some(Protocol::Tlsv12))
.request_alpns(alpn)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
let tls = if strict_tls {
tls_builder
stream: impl SessionStream + 'static,
) -> Result<impl SessionStream> {
if strict_tls {
let tls_stream = wrap_rustls(hostname, alpn, stream).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
} else {
tls_builder
// We use native_tls because it accepts 1024-bit RSA keys.
// Rustls does not support them even if
// certificate checks are disabled: <https://github.com/rustls/rustls/issues/234>.
let tls = async_native_tls::TlsConnector::new()
.min_protocol_version(Some(async_native_tls::Protocol::Tlsv12))
.request_alpns(alpn)
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
};
let tls_stream = tls.connect(hostname, stream).await?;
Ok(tls_stream)
.danger_accept_invalid_certs(true);
let tls_stream = tls.connect(hostname, stream).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
}
}
pub async fn wrap_rustls<T: AsyncRead + AsyncWrite + Unpin>(
pub async fn wrap_rustls(
hostname: &str,
alpn: &[&str],
stream: T,
) -> Result<tokio_rustls::client::TlsStream<T>> {
stream: impl SessionStream,
) -> Result<impl SessionStream> {
let mut root_cert_store = rustls::RootCertStore::empty();
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
+22 -17
View File
@@ -121,7 +121,7 @@ impl Peerstate {
last_seen_autocrypt: last_seen,
prefer_encrypt,
public_key: Some(public_key.clone()),
public_key_fingerprint: Some(public_key.fingerprint()),
public_key_fingerprint: Some(public_key.dc_fingerprint()),
gossip_key: None,
gossip_key_fingerprint: None,
gossip_timestamp: 0,
@@ -153,7 +153,7 @@ impl Peerstate {
public_key: None,
public_key_fingerprint: None,
gossip_key: Some(gossip_header.public_key.clone()),
gossip_key_fingerprint: Some(gossip_header.public_key.fingerprint()),
gossip_key_fingerprint: Some(gossip_header.public_key.dc_fingerprint()),
gossip_timestamp: message_time,
verified_key: None,
verified_key_fingerprint: None,
@@ -308,7 +308,7 @@ impl Peerstate {
pub fn recalc_fingerprint(&mut self) {
if let Some(ref public_key) = self.public_key {
let old_public_fingerprint = self.public_key_fingerprint.take();
self.public_key_fingerprint = Some(public_key.fingerprint());
self.public_key_fingerprint = Some(public_key.dc_fingerprint());
if old_public_fingerprint.is_some()
&& old_public_fingerprint != self.public_key_fingerprint
@@ -319,7 +319,7 @@ impl Peerstate {
if let Some(ref gossip_key) = self.gossip_key {
let old_gossip_fingerprint = self.gossip_key_fingerprint.take();
self.gossip_key_fingerprint = Some(gossip_key.fingerprint());
self.gossip_key_fingerprint = Some(gossip_key.dc_fingerprint());
if old_gossip_fingerprint.is_none()
|| self.gossip_key_fingerprint.is_none()
@@ -506,7 +506,7 @@ impl Peerstate {
fingerprint: Fingerprint,
verifier: String,
) -> Result<()> {
if key.fingerprint() == fingerprint {
if key.dc_fingerprint() == fingerprint {
self.verified_key = Some(key);
self.verified_key_fingerprint = Some(fingerprint);
self.verifier = Some(verifier);
@@ -524,7 +524,7 @@ impl Peerstate {
/// do nothing to avoid overwriting secondary verified key
/// which may be different.
pub fn set_secondary_verified_key(&mut self, gossip_key: SignedPublicKey, verifier: String) {
let fingerprint = gossip_key.fingerprint();
let fingerprint = gossip_key.dc_fingerprint();
if self.verified_key_fingerprint.as_ref() != Some(&fingerprint) {
self.secondary_verified_key = Some(gossip_key);
self.secondary_verified_key_fingerprint = Some(fingerprint);
@@ -542,6 +542,8 @@ impl Peerstate {
/// * `old_addr`: Old address of the peerstate in case of an AEAP transition.
pub(crate) async fn save_to_db_ex(&self, sql: &Sql, old_addr: Option<&str>) -> Result<()> {
let trans_fn = |t: &mut rusqlite::Transaction| {
let verified_key_fingerprint =
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex());
if let Some(old_addr) = old_addr {
// We are doing an AEAP transition to the new address and the SQL INSERT below will
// save the existing peerstate as belonging to this new address. We now need to
@@ -551,11 +553,14 @@ impl Peerstate {
// existing peerstate as this would break encryption to it. This is critical for
// non-verified groups -- if we can't encrypt to the old address, we can't securely
// remove it from the group (to add the new one instead).
//
// NB: We check that `verified_key_fingerprint` hasn't changed to protect from
// possible races.
t.execute(
"UPDATE acpeerstates \
SET verified_key=NULL, verified_key_fingerprint='', verifier='' \
WHERE addr=?",
(old_addr,),
"UPDATE acpeerstates
SET verified_key=NULL, verified_key_fingerprint='', verifier=''
WHERE addr=? AND verified_key_fingerprint=?",
(old_addr, &verified_key_fingerprint),
)?;
}
t.execute(
@@ -604,7 +609,7 @@ impl Peerstate {
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
&verified_key_fingerprint,
self.verifier.as_deref().unwrap_or(""),
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
self.secondary_verified_key_fingerprint
@@ -888,12 +893,12 @@ mod tests {
last_seen_autocrypt: 11,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 12,
gossip_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
@@ -913,7 +918,7 @@ mod tests {
.expect("no peerstate found in the database");
assert_eq!(peerstate, peerstate_new);
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.dc_fingerprint())
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
@@ -932,7 +937,7 @@ mod tests {
last_seen_autocrypt: 11,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
gossip_key: None,
gossip_timestamp: 12,
gossip_key_fingerprint: None,
@@ -969,7 +974,7 @@ mod tests {
last_seen_autocrypt: 11,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
gossip_key: None,
gossip_timestamp: 12,
gossip_key_fingerprint: None,
+64 -25
View File
@@ -14,7 +14,7 @@ use pgp::composed::{
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::types::{CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, StringToKey};
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey};
use rand::{thread_rng, CryptoRng, Rng};
use tokio::runtime::Handle;
@@ -41,8 +41,15 @@ enum SignedPublicKeyOrSubkey<'a> {
Subkey(&'a SignedPublicSubKey),
}
impl KeyTrait for SignedPublicKeyOrSubkey<'_> {
fn fingerprint(&self) -> Vec<u8> {
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
fn version(&self) -> pgp::types::KeyVersion {
match self {
Self::Key(k) => k.version(),
Self::Subkey(k) => k.version(),
}
}
fn fingerprint(&self) -> pgp::types::Fingerprint {
match self {
Self::Key(k) => k.fingerprint(),
Self::Subkey(k) => k.fingerprint(),
@@ -62,14 +69,26 @@ impl KeyTrait for SignedPublicKeyOrSubkey<'_> {
Self::Subkey(k) => k.algorithm(),
}
}
}
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
fn created_at(&self) -> &chrono::DateTime<chrono::Utc> {
match self {
Self::Key(k) => k.created_at(),
Self::Subkey(k) => k.created_at(),
}
}
fn expiration(&self) -> Option<u16> {
match self {
Self::Key(k) => k.expiration(),
Self::Subkey(k) => k.expiration(),
}
}
fn verify_signature(
&self,
hash: HashAlgorithm,
data: &[u8],
sig: &[Mpi],
sig: &SignatureBytes,
) -> pgp::errors::Result<()> {
match self {
Self::Key(k) => k.verify_signature(hash, data, sig),
@@ -79,19 +98,27 @@ impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
fn encrypt<R: Rng + CryptoRng>(
&self,
rng: &mut R,
rng: R,
plain: &[u8],
) -> pgp::errors::Result<Vec<Mpi>> {
typ: pgp::types::EskType,
) -> pgp::errors::Result<pgp::types::PkeskBytes> {
match self {
Self::Key(k) => k.encrypt(rng, plain),
Self::Subkey(k) => k.encrypt(rng, plain),
Self::Key(k) => k.encrypt(rng, plain, typ),
Self::Subkey(k) => k.encrypt(rng, plain, typ),
}
}
fn to_writer_old(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
fn serialize_for_hashing(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
match self {
Self::Key(k) => k.to_writer_old(writer),
Self::Subkey(k) => k.to_writer_old(writer),
Self::Key(k) => k.serialize_for_hashing(writer),
Self::Subkey(k) => k.serialize_for_hashing(writer),
}
}
fn public_params(&self) -> &pgp::types::PublicParams {
match self {
Self::Key(k) => k.public_params(),
Self::Subkey(k) => k.public_params(),
}
}
}
@@ -160,9 +187,10 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
let (signing_key_type, encryption_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
KeyGenType::Ed25519 | KeyGenType::Default => {
(PgpKeyType::EdDSA, PgpKeyType::ECDH(ECCCurve::Curve25519))
}
KeyGenType::Ed25519 | KeyGenType::Default => (
PgpKeyType::EdDSALegacy,
PgpKeyType::ECDH(ECCCurve::Curve25519),
),
};
let user_id = format!("<{addr}>");
@@ -199,10 +227,11 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
.build()
.context("failed to build key parameters")?;
let mut rng = thread_rng();
let secret_key = key_params
.generate()
.generate(&mut rng)
.context("failed to generate the key")?
.sign(|| "".into())
.sign(&mut rng, || "".into())
.context("failed to sign secret key")?;
secret_key
.verify()
@@ -261,15 +290,19 @@ pub async fn pk_encrypt(
let mut rng = thread_rng();
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
let signed_msg = lit_msg.sign(skey, || "".into(), HASH_ALGORITHM)?;
let signed_msg = lit_msg.sign(&mut rng, skey, || "".into(), HASH_ALGORITHM)?;
let compressed_msg = if compress {
signed_msg.compress(CompressionAlgorithm::ZLIB)?
} else {
signed_msg
};
compressed_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
compressed_msg.encrypt_to_keys_seipdv1(
&mut rng,
SYMMETRIC_KEY_ALGORITHM,
&pkeys_refs,
)?
} else {
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
};
let encoded_msg = encrypted_msg.to_armored_string(Default::default())?;
@@ -284,7 +317,9 @@ pub fn pk_calc_signature(
plain: &[u8],
private_key_for_signing: &SignedSecretKey,
) -> Result<String> {
let mut rng = thread_rng();
let msg = Message::new_literal_bytes("", plain).sign(
&mut rng,
private_key_for_signing,
|| "".into(),
HASH_ALGORITHM,
@@ -328,7 +363,7 @@ pub fn valid_signature_fingerprints(
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in public_keys_for_validation {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
let fp = pkey.dc_fingerprint();
ret_signature_fingerprints.insert(fp);
}
}
@@ -355,7 +390,7 @@ pub fn pk_validate(
for pkey in public_keys_for_validation {
if standalone_signature.verify(pkey, content).is_ok() {
let fp = DcKey::fingerprint(pkey);
let fp = pkey.dc_fingerprint();
ret.insert(fp);
}
}
@@ -370,8 +405,12 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
tokio::task::spawn_blocking(move || {
let mut rng = thread_rng();
let s2k = StringToKey::new_default(&mut rng);
let msg =
lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?;
let msg = lit_msg.encrypt_with_password_seipdv1(
&mut rng,
s2k,
SYMMETRIC_KEY_ALGORITHM,
|| passphrase,
)?;
let encoded_msg = msg.to_armored_string(Default::default())?;
-2
View File
@@ -288,8 +288,6 @@ pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::test_utils::TestContext;
+41 -31
View File
@@ -270,6 +270,7 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
/// The function should be called after a QR code is scanned.
/// The function takes the raw text scanned and checks what can be done with it.
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
let qr = qr.trim();
let qrcode = if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
.await
@@ -367,9 +368,10 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
#[allow(clippy::indexing_slicing)]
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
let payload = qr
.get(OPENPGP4FPR_SCHEME.len()..)
.context("Invalid OPENPGP4FPR scheme")?;
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
@@ -546,7 +548,7 @@ async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<
fn decode_account(qr: &str) -> Result<Qr> {
let payload = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("invalid DCACCOUNT payload")?;
.context("Invalid DCACCOUNT payload")?;
let url = url::Url::parse(payload).context("Invalid account URL")?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::Account {
@@ -564,7 +566,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
let payload = qr
.get(DCWEBRTC_SCHEME.len()..)
.context("invalid DCWEBRTC payload")?;
.context("Invalid DCWEBRTC payload")?;
let (_type, url) = Message::parse_webrtc_instance(payload);
let url = url::Url::parse(&url).context("Invalid WebRTC instance")?;
@@ -637,7 +639,7 @@ fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
fn decode_backup2(qr: &str) -> Result<Qr> {
let payload = qr
.strip_prefix(DCBACKUP2_SCHEME)
.ok_or_else(|| anyhow!("invalid DCBACKUP scheme"))?;
.ok_or_else(|| anyhow!("Invalid DCBACKUP2 scheme"))?;
let (auth_token, node_addr) = payload
.split_once('&')
.context("Backup QR code has no separator")?;
@@ -668,9 +670,10 @@ struct CreateAccountErrorResponse {
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
/// on success, a configure::configure() should be able to log in to the account
#[allow(clippy::indexing_slicing)]
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let url_str = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("Invalid DCACCOUNT scheme")?;
if !url_str.starts_with(HTTPS_SCHEME) {
bail!("DCACCOUNT QR codes must use HTTPS scheme");
@@ -800,15 +803,12 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
/// Extract address for the mailto scheme.
///
/// Scheme: `mailto:addr...?subject=...&body=..`
#[allow(clippy::indexing_slicing)]
async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
let payload = &qr[MAILTO_SCHEME.len()..];
let payload = qr
.get(MAILTO_SCHEME.len()..)
.context("Invalid mailto: scheme")?;
let (addr, query) = if let Some(query_index) = payload.find('?') {
(&payload[..query_index], &payload[query_index + 1..])
} else {
(payload, "")
};
let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
let param: BTreeMap<&str, &str> = query
.split('&')
@@ -855,16 +855,12 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
/// Extract address for the smtp scheme.
///
/// Scheme: `SMTP:addr...:subject...:body...`
#[allow(clippy::indexing_slicing)]
async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
let payload = &qr[SMTP_SCHEME.len()..];
let addr = if let Some(query_index) = payload.find(':') {
&payload[..query_index]
} else {
bail!("Invalid SMTP found");
};
let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
let (addr, _rest) = payload
.split_once(':')
.context("Invalid SMTP scheme payload")?;
let addr = normalize_address(addr)?;
let name = "";
Qr::from_address(context, name, &addr, None).await
@@ -875,14 +871,13 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
///
/// There may or may not be linebreaks after the fields.
#[allow(clippy::indexing_slicing)]
async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
// we ignore this case.
let addr = if let Some(to_index) = qr.find("TO:") {
let addr = qr[to_index + 3..].trim();
let addr = qr.get(to_index + 3..).unwrap_or_default().trim();
if let Some(semi_index) = addr.find(';') {
addr[..semi_index].trim()
addr.get(..semi_index).unwrap_or_default().trim()
} else {
addr
}
@@ -903,7 +898,6 @@ static VCARD_EMAIL_RE: Lazy<regex::Regex> =
/// Extract address for the vcard scheme.
///
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
#[allow(clippy::indexing_slicing)]
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
let name = VCARD_NAME_RE
.captures(qr)
@@ -915,8 +909,8 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
})
.unwrap_or_default();
let addr = if let Some(caps) = VCARD_EMAIL_RE.captures(qr) {
normalize_address(caps[2].trim())?
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
normalize_address(cap.as_str().trim())?
} else {
bail!("Bad e-mail address");
};
@@ -1001,6 +995,17 @@ mod tests {
}
);
// Test that QR code whitespace is stripped.
// Users can copy-paste QR code contents and "scan"
// from the clipboard.
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
assert_eq!(
qr,
Qr::Url {
url: "http://www.hello.com/hello".to_string(),
}
);
Ok(())
}
@@ -1309,7 +1314,7 @@ mod tests {
last_seen_autocrypt: 1,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
gossip_key: None,
gossip_timestamp: 0,
gossip_key_fingerprint: None,
@@ -1340,7 +1345,10 @@ mod tests {
let qr = check_qr(
&ctx.ctx,
&format!("OPENPGP4FPR:{}#a=alice@example.org", pub_key.fingerprint()),
&format!(
"OPENPGP4FPR:{}#a=alice@example.org",
pub_key.dc_fingerprint()
),
)
.await?;
if let Qr::FprOk { contact_id, .. } = qr {
@@ -1747,7 +1755,9 @@ mod tests {
);
// Test URL without port.
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
//
// Also check that whitespace is trimmed.
let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
assert_eq!(
+42 -13
View File
@@ -558,7 +558,12 @@ Here's my footer -- bob@example.net"
) -> Result<()> {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::ReactionsChanged { .. }))
.get_matching(|evt| {
matches!(
evt,
EventType::ReactionsChanged { .. } | EventType::IncomingMsg { .. }
)
})
.await;
match event {
EventType::ReactionsChanged {
@@ -570,7 +575,7 @@ Here's my footer -- bob@example.net"
assert_eq!(msg_id, expected_msg_id);
assert_eq!(contact_id, expected_contact_id);
}
_ => unreachable!(),
_ => panic!("Unexpected event {event:?}."),
}
Ok(())
}
@@ -583,7 +588,14 @@ Here's my footer -- bob@example.net"
) -> Result<()> {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingReaction { .. }))
// Check for absence of `IncomingMsg` events -- it appeared that it's quite easy to make
// bugs when `IncomingMsg` is issued for reactions.
.get_matching(|evt| {
matches!(
evt,
EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
)
})
.await;
match event {
EventType::IncomingReaction {
@@ -595,16 +607,25 @@ Here's my footer -- bob@example.net"
assert_eq!(contact_id, expected_contact_id);
assert_eq!(reaction, Reaction::from(expected_reaction));
}
_ => unreachable!(),
_ => panic!("Unexpected event {event:?}."),
}
Ok(())
}
async fn has_incoming_reactions_event(t: &TestContext) -> bool {
t.evtracker
.get_matching_opt(t, |evt| matches!(evt, EventType::IncomingReaction { .. }))
.await
.is_some()
/// Checks that no unwanted events remain after expecting "wanted" reaction events.
async fn expect_no_unwanted_events(t: &TestContext) {
let ev = t
.evtracker
.get_matching_opt(t, |evt| {
matches!(
evt,
EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
)
})
.await;
if let Some(ev) = ev {
panic!("Unwanted event {ev:?}.")
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -635,9 +656,10 @@ Here's my footer -- bob@example.net"
bob_msg.chat_id.accept(&bob).await?;
bob.evtracker.clear_events();
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
assert!(!has_incoming_reactions_event(&bob).await);
expect_no_unwanted_events(&bob).await;
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
let bob_reaction_msg = bob.pop_sent_msg().await;
@@ -656,6 +678,7 @@ Here's my footer -- bob@example.net"
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
.await?;
expect_incoming_reactions_event(&alice, alice_msg.sender_msg_id, *bob_id, "👍").await?;
expect_no_unwanted_events(&alice).await;
// Alice reacts to own message.
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
@@ -684,6 +707,7 @@ Here's my footer -- bob@example.net"
let bob = TestContext::new_bob().await;
alice.set_config(Config::Displayname, Some("ALICE")).await?;
bob.set_config(Config::Displayname, Some("BOB")).await?;
let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await;
// Alice sends message to Bob
let alice_chat = alice.create_chat(&bob).await;
@@ -696,7 +720,9 @@ Here's my footer -- bob@example.net"
send_reaction(&bob, bob_msg1.id, "👍").await?;
let bob_send_reaction = bob.pop_sent_msg().await;
alice.recv_msg_trash(&bob_send_reaction).await;
assert!(has_incoming_reactions_event(&alice).await);
expect_incoming_reactions_event(&alice, alice_msg1.sender_msg_id, alice_bob_id, "👍")
.await?;
expect_no_unwanted_events(&alice).await;
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
let summary = chatlist.get_summary(&bob, 0, None).await?;
@@ -711,8 +737,9 @@ Here's my footer -- bob@example.net"
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?;
let alice_send_reaction = alice.pop_sent_msg().await;
bob.evtracker.clear_events();
bob.recv_msg_opt(&alice_send_reaction).await;
assert!(!has_incoming_reactions_event(&bob).await);
expect_no_unwanted_events(&bob).await;
assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await;
assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await;
@@ -934,7 +961,9 @@ Here's my footer -- bob@example.net"
expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
.await?;
for a in [&alice0, &alice1] {
expect_no_unwanted_events(a).await;
}
Ok(())
}
}
+55 -32
View File
@@ -6,7 +6,7 @@ use std::str::FromStr;
use anyhow::{Context as _, Result};
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress};
use iroh_gossip::proto::TopicId;
use mailparse::{parse_mail, SingleInfo};
use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use once_cell::sync::Lazy;
use regex::Regex;
@@ -78,7 +78,7 @@ pub async fn receive_imf(
imf_raw: &[u8],
seen: bool,
) -> Result<Option<ReceivedMsg>> {
let mail = parse_mail(imf_raw).context("can't parse mail")?;
let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
let rfc724_mid =
imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
if let Some(download_limit) = context.download_limit().await? {
@@ -1423,7 +1423,11 @@ async fn add_parts(
if let Some(msg) = group_changes_msgs.1 {
match &better_msg {
None => better_msg = Some(msg),
Some(_) => group_changes_msgs.0.push(msg),
Some(_) => {
if !msg.is_empty() {
group_changes_msgs.0.push(msg)
}
}
}
}
@@ -1506,6 +1510,9 @@ async fn add_parts(
let mut txt_raw = "".to_string();
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
if better_msg.is_empty() && is_partial_download.is_none() {
chat_id = DC_CHAT_ID_TRASH;
}
(better_msg, Viewtype::Text)
} else {
(&part.msg, part.typ)
@@ -1568,7 +1575,7 @@ INSERT INTO msgs
ON CONFLICT (id) DO UPDATE
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
type=excluded.type, msgrmsg=excluded.msgrmsg,
type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
txt_raw=excluded.txt_raw, param=excluded.param,
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
@@ -2077,8 +2084,11 @@ async fn create_group(
/// Apply group member list, name, avatar and protection status changes from the MIME message.
///
/// Optionally returns better message to replace the original system message.
/// is_partial_download: whether the message is not fully downloaded.
/// Returns `Vec` of group changes messages and, optionally, a better message to replace the
/// original system message. If the better message is empty, the original system message should be
/// just omitted.
///
/// * `is_partial_download` - whether the message is not fully downloaded.
#[allow(clippy::too_many_arguments)]
async fn apply_group_changes(
context: &Context,
@@ -2180,39 +2190,47 @@ async fn apply_group_changes(
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
better_msg = if removed_id == Some(from_id) {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
if removed_id.is_some() {
if !allow_member_list_changes {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
);
if let Some(id) = removed_id {
if allow_member_list_changes && chat_contacts.contains(&id) {
better_msg = if id == from_id {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
}
} else {
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
better_msg.get_or_insert_with(Default::default);
if !allow_member_list_changes {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
);
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if allow_member_list_changes {
if !recreate_member_list {
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
let is_new_member;
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
if !recreate_member_list {
added_id = Some(contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.")
}
is_new_member = !chat_contacts.contains(&contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.");
is_new_member = false;
}
if is_new_member || self_added {
better_msg =
Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
}
} else {
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
}
better_msg.get_or_insert_with(Default::default);
} else if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.map(|s| s.trim())
@@ -2447,14 +2465,16 @@ async fn create_or_lookup_mailinglist(
}
}
#[allow(clippy::indexing_slicing)]
fn compute_mailinglist_name(
list_id_header: &str,
listid: &str,
mime_parser: &MimeMessage,
) -> String {
let mut name = match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap[1].trim().to_string(),
let mut name = match LIST_ID_REGEX
.captures(list_id_header)
.and_then(|caps| caps.get(1))
{
Some(cap) => cap.as_str().trim().to_string(),
None => "".to_string(),
};
@@ -2501,8 +2521,11 @@ fn compute_mailinglist_name(
// 51231231231231231231231232869f58.xing.com -> xing.com
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
if let Some(cap) = PREFIX_32_CHARS_HEX.captures(listid) {
name = cap[2].to_string();
if let Some(cap) = PREFIX_32_CHARS_HEX
.captures(listid)
.and_then(|caps| caps.get(2))
{
name = cap.as_str().to_string();
} else {
name = listid.to_string();
}
+59 -3
View File
@@ -1068,7 +1068,7 @@ async fn test_classic_mailing_list() -> Result<()> {
let mime = sent.payload();
println!("Sent mime message is:\n\n{mime}\n\n");
assert!(mime.contains("Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no\r\n"));
assert!(mime.contains("Content-Type: text/plain; charset=utf-8\r\n"));
assert!(mime.contains("Subject: =?utf-8?q?Re=3A_=5Bdelta-dev=5D_What=27s_up=3F?=\r\n"));
assert!(mime.contains("MIME-Version: 1.0\r\n"));
assert!(mime.contains("In-Reply-To: <38942@posteo.org>\r\n"));
@@ -4185,9 +4185,8 @@ async fn test_recreate_contact_list_on_missing_message() -> Result<()> {
// readd fiona
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
alice.recv_msg(&remove_msg).await;
// delayed removal of fiona shouldn't remove her
alice.recv_msg_trash(&remove_msg).await;
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
Ok(())
@@ -4856,6 +4855,37 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protected_group_reply_from_mua() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
mark_as_verified(alice, bob).await;
mark_as_verified(alice, fiona).await;
mark_as_verified(bob, alice).await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob, fiona])
.await;
let sent = alice.send_text(alice_chat_id, "Hello!").await;
let bob_msg = bob.recv_msg(&sent).await;
bob_msg.chat_id.accept(bob).await?;
// This is hacky, but i don't know other simple way to simulate a MUA reply. It works because
// the message is correctly assigned to the chat by `References:`.
bob.sql
.execute(
"UPDATE chats SET protected=0, grpid='' WHERE id=?",
(bob_msg.chat_id,),
)
.await?;
let sent = bob
.send_text(bob_msg.chat_id, "/me replying from MUA")
.await;
let alice_msg = alice.recv_msg(&sent).await;
assert_eq!(alice_msg.chat_id, alice_chat_id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_older_message_from_2nd_device() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -4947,6 +4977,32 @@ async fn test_unarchive_on_member_removal() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_op_member_added_is_trash() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "foos", &[bob])
.await;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
let msg = alice.pop_sent_msg().await;
bob.recv_msg(&msg).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
bob_chat_id.accept(bob).await?;
let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
let msg = alice.pop_sent_msg().await;
let fiona_id = Contact::create(bob, "", "fiona@example.net").await?;
add_contact_to_chat(bob, bob_chat_id, fiona_id).await?;
bob.recv_msg_trash(&msg).await;
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 3);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forged_from() -> Result<()> {
let mut tcm = TestContextManager::new();
+4 -5
View File
@@ -318,10 +318,6 @@ impl Context {
.yellow {
background-color: #fdc625;
}
.not-started-error {
font-size: 2em;
color: red;
}
</style>
</head>
<body>"#
@@ -341,7 +337,10 @@ impl Context {
sched.smtp.state.connectivity.clone(),
),
_ => {
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";
ret += &format!(
"<h3>{}</h3>\n</body></html>\n",
stock_str::not_connected(self).await
);
return Ok(ret);
}
};
+16 -14
View File
@@ -104,7 +104,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
context.scheduler.interrupt_inbox().await;
}
format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
&group_name_urlencoded,
@@ -119,7 +119,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
context.scheduler.interrupt_inbox().await;
}
format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
"https://i.delta.chat/#{}&a={}&n={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
self_name_urlencoded,
@@ -136,7 +136,7 @@ async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
let key = load_self_public_key(context)
.await
.context("Failed to load key")?;
Ok(key.fingerprint())
Ok(key.dc_fingerprint())
}
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
@@ -244,10 +244,10 @@ async fn verify_sender_by_fingerprint(
/// What to do with a Secure-Join handshake message after it was handled.
///
/// This status is returned to [`receive_imf`] which will use it to decide what to do
/// This status is returned to [`receive_imf_inner`] which will use it to decide what to do
/// next with this incoming setup-contact/secure-join handshake message.
///
/// [`receive_imf`]: crate::receive_imf::receive_imf
/// [`receive_imf_inner`]: crate::receive_imf::receive_imf_inner
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum HandshakeMessage {
/// The message has been fully handled and should be removed/delete.
@@ -279,7 +279,6 @@ pub(crate) enum HandshakeMessage {
///
/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
/// database; this is done by `receive_imf()` later on as needed.
#[allow(clippy::indexing_slicing)]
pub(crate) async fn handle_securejoin_handshake(
context: &Context,
mime_message: &MimeMessage,
@@ -298,9 +297,9 @@ pub(crate) async fn handle_securejoin_handshake(
if !matches!(step, "vg-request" | "vc-request") {
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.fingerprint();
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
for (addr, key) in &mime_message.gossiped_keys {
if key.fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
self_found = true;
break;
}
@@ -348,7 +347,7 @@ pub(crate) async fn handle_securejoin_handshake(
send_alice_handshake_msg(
context,
contact_id,
&format!("{}-auth-required", &step[..2]),
&format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
)
.await
.context("failed sending auth-required handshake message")?;
@@ -936,7 +935,10 @@ mod tests {
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = load_self_public_key(&bob.ctx).await.unwrap().fingerprint();
let bob_fp = load_self_public_key(&bob.ctx)
.await
.unwrap()
.dc_fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
@@ -1106,10 +1108,10 @@ mod tests {
last_seen_autocrypt: 10,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(alice_pubkey.clone()),
public_key_fingerprint: Some(alice_pubkey.fingerprint()),
public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
gossip_key: Some(alice_pubkey.clone()),
gossip_timestamp: 10,
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
verified_key: None,
verified_key_fingerprint: None,
verifier: None,
@@ -1157,7 +1159,7 @@ mod tests {
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint();
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
@@ -1320,7 +1322,7 @@ mod tests {
"vg-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint();
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
+1 -1
View File
@@ -389,7 +389,7 @@ async fn send_handshake_message(
msg.param.set_int(Param::GuaranteeE2ee, 1);
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
let bob_fp = load_self_public_key(context).await?.fingerprint();
let bob_fp = load_self_public_key(context).await?.dc_fingerprint();
msg.param.set(Param::Arg3, bob_fp.hex());
// Sends the grpid in the Secure-Join-Group header.
+51 -21
View File
@@ -1,4 +1,5 @@
//! # Simplify incoming plaintext.
use crate::tools::IsNoneOrEmpty;
/// Protects lines starting with `--` against being treated as a footer.
/// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
@@ -20,7 +21,6 @@ pub fn escape_message_footer_marks(text: &str) -> String {
/// Returns `(lines, footer_lines)` tuple;
/// `footer_lines` is set to `Some` if the footer was actually removed from `lines`
/// (which is equal to the input array otherwise).
#[allow(clippy::indexing_slicing)]
pub(crate) fn remove_message_footer<'a>(
lines: &'a [&str],
) -> (&'a [&'a str], Option<&'a [&'a str]>) {
@@ -28,14 +28,13 @@ pub(crate) fn remove_message_footer<'a>(
for (ix, &line) in lines.iter().enumerate() {
match line {
// some providers encode `-- ` to `-- =20` which results in `-- `
"-- " | "-- " => return (&lines[..ix], lines.get(ix + 1..)),
"-- " | "-- " => return (lines.get(..ix).unwrap_or(lines), lines.get(ix + 1..)),
// some providers encode `-- ` to `=2D-` which results in only `--`;
// use that only when no other footer is found
// and if the line before is empty and the line after is not empty
"--" => {
if (ix == 0 || lines[ix - 1].is_empty())
&& ix != lines.len() - 1
&& !lines[ix + 1].is_empty()
if (ix == 0 || lines.get(ix.saturating_sub(1)).is_none_or_empty())
&& !lines.get(ix + 1).is_none_or_empty()
{
nearly_standard_footer = Some(ix);
}
@@ -44,7 +43,7 @@ pub(crate) fn remove_message_footer<'a>(
}
}
if let Some(ix) = nearly_standard_footer {
return (&lines[..ix], lines.get(ix + 1..));
return (lines.get(..ix).unwrap_or(lines), lines.get(ix + 1..));
}
(lines, None)
}
@@ -53,7 +52,6 @@ pub(crate) fn remove_message_footer<'a>(
/// Returns `(lines, is_footer_removed)` tuple;
/// `is_footer_removed` is set to `true` if the footer was actually removed from `lines`
/// (which is equal to the input array otherwise).
#[allow(clippy::indexing_slicing)]
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
for (ix, &line) in lines.iter().enumerate() {
if line == "--"
@@ -63,7 +61,10 @@ fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|| line.starts_with("*****")
|| line.starts_with("~~~~~")
{
return (&lines[..ix], true);
// `get` should always return `Some` here.
if let Some(lines) = lines.get(..ix) {
return (lines, true);
}
}
}
(lines, false)
@@ -173,7 +174,6 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
}
}
#[allow(clippy::indexing_slicing)]
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
let mut first_quoted_line = lines.len();
let mut last_quoted_line = None;
@@ -188,30 +188,36 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
}
}
if let Some(mut l_last) = last_quoted_line {
let quoted_text = lines[l_last..first_quoted_line]
let quoted_text = lines
.iter()
.take(first_quoted_line)
.skip(l_last)
.map(|s| {
s.strip_prefix('>')
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
})
.collect::<Vec<&str>>()
.join("\n");
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
if l_last > 1 {
let line = lines[l_last - 1];
if is_quoted_headline(line) {
l_last -= 1
if let Some(line) = lines.get(l_last - 1) {
if is_empty_line(line) {
l_last -= 1
}
}
}
(&lines[..l_last], Some(quoted_text))
if l_last > 1 {
if let Some(line) = lines.get(l_last - 1) {
if is_quoted_headline(line) {
l_last -= 1
}
}
}
(lines.get(..l_last).unwrap_or(lines), Some(quoted_text))
} else {
(lines, None)
}
}
#[allow(clippy::indexing_slicing)]
fn remove_top_quote<'a>(
lines: &'a [&str],
is_chat_message: bool,
@@ -238,10 +244,12 @@ fn remove_top_quote<'a>(
}
if let Some(last_quoted_line) = last_quoted_line {
(
&lines[last_quoted_line + 1..],
lines.get(last_quoted_line + 1..).unwrap_or(lines),
Some(
lines[first_quoted_line..last_quoted_line + 1]
lines
.iter()
.take(last_quoted_line + 1)
.skip(first_quoted_line)
.map(|s| {
s.strip_prefix('>')
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
@@ -298,7 +306,7 @@ fn is_quoted_headline(buf: &str) -> bool {
- Currently, we simply check if the last character is a ':'.
- Checking for the existence of an email address may fail (headlines may show the user's name instead of the address) */
buf.len() <= 80 && buf.ends_with(':')
buf.len() <= 120 && buf.ends_with(':')
}
fn is_plain_quote(buf: &str) -> bool {
@@ -403,6 +411,28 @@ mod tests {
assert!(!is_plain_quote(""));
}
#[test]
fn test_is_quoted_headline() {
assert!(is_quoted_headline("On 2024-08-28, Bob wrote:"));
assert!(is_quoted_headline("Am 11. November 2024 schrieb Alice:"));
assert!(is_quoted_headline("Anonymous Longer Name a écrit:"));
assert!(is_quoted_headline("There is not really a pattern wrote:"));
assert!(is_quoted_headline(
"On Mon, 3 Jan, 2022 at 8:34 PM \"Anonymous Longer Name\" <anonymous-longer-name@example.com> wrote:"
));
assert!(!is_quoted_headline(
"How are you? I just want to say that this line does not belong to the quote!"
));
assert!(!is_quoted_headline(
"No quote headline as not ending with a colon"
));
assert!(!is_quoted_headline(
"Even though this ends with a colon, \
this is no quote-headline as just too long for most cases of date+name+address. \
it's all heuristics only, it is expected to go wrong sometimes. there is always the 'Show full message' button:"
));
}
#[test]
fn test_remove_top_quote() {
let (lines, top_quote) = remove_top_quote(&["> first", "> second"], true);
+21 -26
View File
@@ -244,32 +244,27 @@ pub(crate) async fn smtp_send(
async_smtp::error::Error::Transient(ref response) => {
// We got a transient 4xx response from SMTP server.
// Give some time until the server-side error maybe goes away.
if let Some(first_word) = response.first_word() {
if first_word.ends_with(".1.1")
|| first_word.ends_with(".1.2")
|| first_word.ends_with(".1.3")
{
// Sometimes we receive transient errors that should be permanent.
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
// receive as a transient error are misconfigurations of the smtp server.
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
info!(context, "Received extended status code {first_word} for a transient error. This looks like a misconfigured SMTP server, let's fail immediately.");
SendResult::Failure(format_err!("Permanent SMTP error: {}", err))
} else {
info!(
context,
"Transient error with status code {first_word}, postponing retry for later."
);
SendResult::Retry
}
} else {
info!(
context,
"Transient error without status code, postponing retry for later."
);
SendResult::Retry
}
//
// One particular case is
// `450 4.1.2 <alice@example.org>: Recipient address rejected: Domain not found`.
// known to be returned by Postfix.
//
// [RFC 3463](https://tools.ietf.org/html/rfc3463#section-3.2)
// says "This code is only useful for permanent failures."
// in X.1.1, X.1.2 and X.1.3 descriptions.
//
// Previous Delta Chat core versions
// from 1.51.0 to 1.151.1
// were treating such errors as permanent.
//
// This was later reverted because such errors were observed
// for existing domains and turned out to be actually transient,
// likely caused by nameserver downtime.
info!(
context,
"Transient error {response:?}, postponing retry for later."
);
SendResult::Retry
}
_ => {
info!(
+3 -6
View File
@@ -942,15 +942,12 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
Ok(())
}
#[allow(clippy::indexing_slicing)]
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
let name_to_check = if let Some(namespc) = namespc_opt {
let name_len = name.len();
let namespc_len = namespc.len();
if name_len <= namespc_len || !name.ends_with(namespc) {
let Some(name) = name.strip_suffix(namespc) else {
return false;
}
&name[..name_len - namespc_len]
};
name
} else {
name
};
+2 -2
View File
@@ -289,7 +289,7 @@ mod tests {
use super::*;
use crate::chat::ChatId;
use crate::param::Param;
use crate::test_utils as test;
use crate::test_utils::TestContext;
async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
assert_eq!(msg.get_summary_text(ctx).await, expected);
@@ -298,7 +298,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_text() {
let d = test::TestContext::new().await;
let d = TestContext::new_alice().await;
let ctx = &d.ctx;
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
.await
+13 -4
View File
@@ -1,7 +1,6 @@
//! Utilities to help writing tests.
//!
//! This private module is only compiled for test runs.
#![allow(clippy::indexing_slicing)]
use std::collections::{BTreeMap, HashSet};
use std::fmt::Write;
use std::ops::{Deref, DerefMut};
@@ -392,7 +391,7 @@ impl TestContext {
Self {
ctx,
dir,
evtracker: EventTracker(evtracker_receiver),
evtracker: EventTracker::new(evtracker_receiver),
_log_sink,
}
}
@@ -656,8 +655,8 @@ impl TestContext {
.expect("failed to load msg")
}
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
/// Returns the [`ContactId`] for the other [`TestContext`], creating a contact if necessary.
pub async fn add_or_lookup_contact_id(&self, other: &TestContext) -> ContactId {
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
let addr = ContactAddress::new(&primary_self_addr).unwrap();
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
@@ -671,6 +670,12 @@ impl TestContext {
Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr),
Modifier::Created => warn!(&self.ctx, "Contact {} created by TestContext", &addr),
}
contact_id
}
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
let contact_id = self.add_or_lookup_contact_id(other).await;
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
}
@@ -1088,6 +1093,10 @@ impl DerefMut for EventTracker {
}
impl EventTracker {
pub fn new(emitter: EventEmitter) -> Self {
Self(emitter)
}
/// Consumes emitted events returning the first matching one.
///
/// If no matching events are ready this will wait for new events to arrive and time out
+1
View File
@@ -1,2 +1,3 @@
mod account_events;
mod aeap;
mod verified_chats;
+170
View File
@@ -0,0 +1,170 @@
//! contains tests for account (list) events
use std::time::Duration;
use anyhow::Result;
use tempfile::tempdir;
use crate::accounts::Accounts;
use crate::config::Config;
use crate::imex::{get_backup, has_backup, imex, BackupProvider, ImexMode};
use crate::test_utils::{sync, EventTracker, TestContext, TestContextManager};
use crate::EventType;
async fn wait_for_item_changed(context: &TestContext) {
context
.evtracker
.get_matching(|evt| matches!(evt, EventType::AccountsItemChanged))
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_event() -> Result<()> {
let dir = tempdir().unwrap();
let mut manager = Accounts::new(dir.path().join("accounts"), true).await?;
let tracker = EventTracker::new(manager.get_event_emitter());
// create account
tracker.clear_events();
let account_id = manager.add_account().await?;
tracker
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
.await;
// remove account
tracker.clear_events();
manager.remove_account(account_id).await?;
tracker
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
.await;
// create closed account
tracker.clear_events();
manager.add_closed_account().await?;
tracker
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
.await;
Ok(())
}
// configuration is tested by python tests in deltachat-rpc-client/tests/test_account_events.py
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_displayname() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = tcm.alice().await;
context.evtracker.clear_events();
context
.set_config(crate::config::Config::Displayname, Some("🐰 Alice"))
.await?;
wait_for_item_changed(&context).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_selfavatar() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = tcm.alice().await;
let file = context.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;
context.evtracker.clear_events();
context
.set_config(
crate::config::Config::Selfavatar,
Some(file.to_str().unwrap()),
)
.await?;
wait_for_item_changed(&context).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_private_tag() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = tcm.alice().await;
context.evtracker.clear_events();
context
.set_config(crate::config::Config::PrivateTag, Some("Wonderland"))
.await?;
wait_for_item_changed(&context).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_import_backup() -> Result<()> {
let mut tcm = TestContextManager::new();
let context1 = tcm.alice().await;
let backup_dir = tempfile::tempdir().unwrap();
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_ok()
);
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
context2.evtracker.clear_events();
let backup = has_backup(&context2, backup_dir.path()).await?;
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None).await?;
assert!(context2.is_configured().await?);
wait_for_item_changed(&context2).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_backup() {
let mut tcm = TestContextManager::new();
// Create first device.
let ctx0 = tcm.alice().await;
// Prepare to transfer backup.
let provider = BackupProvider::prepare(&ctx0).await.unwrap();
// Set up second device.
let ctx1 = tcm.unconfigured().await;
ctx1.evtracker.clear_events();
get_backup(&ctx1, provider.qr()).await.unwrap();
// Make sure the provider finishes without an error.
tokio::time::timeout(Duration::from_secs(30), provider)
.await
.expect("timed out")
.expect("error in provider");
wait_for_item_changed(&ctx1).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync() -> Result<()> {
let alice0 = TestContext::new_alice().await;
let alice1 = TestContext::new_alice().await;
for a in [&alice0, &alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let new_name = "new name";
alice0
.set_config(Config::Displayname, Some(new_name))
.await?;
alice1.evtracker.clear_events();
sync(&alice0, &alice1).await;
wait_for_item_changed(&alice1).await;
assert_eq!(
alice1.get_config(Config::Displayname).await?,
Some(new_name.to_owned())
);
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice1.evtracker.clear_events();
sync(&alice0, &alice1).await;
wait_for_item_changed(&alice1).await;
Ok(())
}
+39 -4
View File
@@ -1,15 +1,13 @@
#![allow(clippy::indexing_slicing)]
use anyhow::Result;
use crate::chat;
use crate::chat::ChatId;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::contact;
use crate::contact::Contact;
use crate::contact::ContactId;
use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::stock_str;
use crate::test_utils::mark_as_verified;
use crate::test_utils::TestContext;
@@ -396,3 +394,40 @@ async fn test_aeap_replay_attack() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_write_to_alice_after_aeap() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_grp_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let bob_alice_contact = bob.add_or_lookup_contact(alice).await;
assert!(bob_alice_contact.is_verified(bob).await?);
let bob_alice_chat = bob.create_chat(alice).await;
assert!(bob_alice_chat.is_protected());
let bob_unprotected_grp_id = bob
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[alice])
.await;
tcm.change_addr(alice, "alice@someotherdomain.xyz").await;
let sent = alice.send_text(alice_grp_id, "Hello!").await;
bob.recv_msg(&sent).await;
assert!(!bob_alice_contact.is_verified(bob).await?);
let bob_alice_chat = Chat::load_from_db(bob, bob_alice_chat.id).await?;
assert!(bob_alice_chat.is_protected());
let mut msg = Message::new_text("hi".to_string());
assert!(chat::send_msg(bob, bob_alice_chat.id, &mut msg)
.await
.is_err());
// But encrypted communication is still possible in unprotected groups with old Alice.
let sent = bob
.send_text(bob_unprotected_grp_id, "Alice, how is your address change?")
.await;
let msg = Message::load_from_db(bob, sent.sender_msg_id).await?;
assert!(msg.get_showpadlock());
Ok(())
}
+19 -15
View File
@@ -45,23 +45,29 @@ use crate::stock_str;
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
#[allow(clippy::indexing_slicing)]
pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let count = buf.chars().count();
if count > approx_chars + DC_ELLIPSIS.len() {
let end_pos = buf
.char_indices()
.nth(approx_chars)
.map(|(n, _)| n)
.unwrap_or_default();
if count <= approx_chars + DC_ELLIPSIS.len() {
return Cow::Borrowed(buf);
}
let end_pos = buf
.char_indices()
.nth(approx_chars)
.map(|(n, _)| n)
.unwrap_or_default();
if let Some(index) = buf[..end_pos].rfind([' ', '\n']) {
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSIS))
} else {
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSIS))
}
if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) {
Cow::Owned(format!(
"{}{}",
&buf.get(..=index).unwrap_or_default(),
DC_ELLIPSIS
))
} else {
Cow::Borrowed(buf)
Cow::Owned(format!(
"{}{}",
&buf.get(..end_pos).unwrap_or_default(),
DC_ELLIPSIS
))
}
}
@@ -709,8 +715,6 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use chrono::NaiveDate;
use proptest::prelude::*;
+477 -120
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -146,9 +146,11 @@ pub(crate) async fn intercept_get_updates(
item: StatusUpdateItem {
payload: serde_json::to_value(location_item)?,
info: None,
href: None,
document: None,
summary: None,
uid: None,
notify: None,
},
serial: StatusUpdateSerial(location.location_id),
max_serial: StatusUpdateSerial(location.location_id),
@@ -203,7 +205,6 @@ mod tests {
t.send_webxdc_status_update(
integration_id,
r#"{"payload": {"action": "pos", "lat": 11.0, "lng": 12.0, "label": "poi #1"}}"#,
"descr",
)
.await?;
t.evtracker
@@ -237,7 +238,6 @@ mod tests {
t.send_webxdc_status_update(
integration_id,
r#"{"payload": {"action": "pos", "lat": 22.0, "lng": 23.0, "label": "poi #2"}}"#,
"descr",
)
.await?;
let updates = t