Compare commits

..

68 Commits

Author SHA1 Message Date
Septias
c68e25a970 fix tests 2024-11-22 10:26:27 +01:00
Septias
e04c4446d6 fix: Disable setting draft if not in chat 2024-11-20 12:52:33 +01:00
Septias
1b9f3368fa fix: only show draft in message info if self is in chat
This PR adds a check to `get_info` so that `draft` is only added if self
is part of the group determined by `can_send`.

close #6229
2024-11-20 12:28:03 +01: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ähn
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
bjoern
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
link2xt
800edc6fce test: remove all calls to print() from deltachat-rpc-client tests
They frequently fail in CI with `OSError: [Errno 9] Bad file descriptor`.
2024-11-07 01:42:01 +00:00
iequidoo
4e5e9f6006 fix: send_msg_to_smtp: Return Ok if smtp row is deleted in parallel
Follow-up to ded8c02c0f. `smtp` rows may be deleted in parallel, in
this case there's just nothing to send.
2024-11-06 21:25:15 -03:00
link2xt
d9d694ead0 fix: remove footers from "Show Full Message..." 2024-11-07 00:24:21 +00:00
link2xt
faad576d10 feat: experimental header protection for Autocrypt
This change adds support for receiving
Autocrypt header in the protected part of encrypted message.

Autocrypt header is now also allowed in mailing lists.
Previously Autocrypt header was rejected when
List-Post header was present,
but the check for the address being equal to the From: address
is sufficient.

New experimental `protect_autocrypt` config is disabled
by default because Delta Chat with reception
support should be released first on all platforms.
2024-11-06 23:16:09 +00:00
Hocuri
b96593ed10 fix: Prevent accidental wrong-password-notifications (#6122)
Over the past years, it happend two times that a user came to me worried
about a false-positive "Cannot login as ***. Please check if the e-mail
address and the password are correct." message.

I'm not sure why this happened, but this PR makes the logic for
showing this notification stricter:
- Before: The notification is shown if connection fails two times in a
row, and the second error contains the word "authentication".
- Now: The notification is shown if the connection fails two times in a
row, and _both_ error messages contain the word "authentication".

The second commit just renames `login_failed_once` to
`authentication_failed_once` in order to reflect this change.
2024-11-05 21:13:21 +00:00
link2xt
d2324a8fc4 chore: fix nightly clippy warnings 2024-11-05 15:05:42 +00:00
link2xt
10a05fa6d9 chore(release): prepare for 1.149.0 2024-11-05 12:08:00 +00:00
link2xt
97d2119028 chore(cargo): update iroh to 0.28.1 2024-11-04 21:01:40 +00:00
link2xt
a510d5f3c2 build: nix flake update android 2024-11-04 20:10:43 +00:00
link2xt
678f1b305c build: update tokio to 1.41 and Android NDK to r27
Delta Chat for Android does not support Android 4 anymore,
so there is no reason to keep using unsupported NDK.

r27 is the latest LTS version of Android NDK.

Tested:
- `nix build .#deltachat-rpc-server-arm64-v8a-android`
- `nix build .#deltachat-rpc-server-armv6l-linux`

`nix build .#deltachat-rpc-server-x86_64-android`
and
`nix build .#deltachat-rpc-server-x86-android`
still fail, but we do not build it in CI.
2024-11-04 20:10:43 +00:00
link2xt
dface33699 chore(release): prepare for 1.148.7 2024-11-03 21:50:59 +00:00
link2xt
92c6dd483c api: add API to reset contact encryption 2024-11-03 02:04:41 +00:00
link2xt
c627d2fcc8 refactor: remove has_decrypted_pgp_armor()
Explicit check for `-----BEGIN PGP MESSAGE-----` is unnecessary
and not sufficient to ensure that the message is valid.
We have already checked the MIME type,
so ASCII-armored OpenPGP message should be inside.
If it's not, decryption will fail anyway.
2024-11-03 01:16:17 +00:00
dependabot[bot]
429c14ae0b Merge pull request #6157 from deltachat/dependabot/cargo/libc-0.2.161 2024-11-02 17:29:07 +00:00
dependabot[bot]
ce40c04e63 Merge pull request #6156 from deltachat/dependabot/cargo/brotli-7.0.0 2024-11-02 17:09:32 +00:00
iequidoo
b89eec8bbb feat: Emit chatlist events only if message still exists
Otherwise, if the message is already deleted, an appropriate chatlist event must be generated and
there's no need in any other events.
2024-11-02 13:55:23 -03:00
dependabot[bot]
7175ee8587 chore(cargo): bump libc from 0.2.159 to 0.2.161
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.159 to 0.2.161.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.161/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.159...0.2.161)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:38:04 +00:00
dependabot[bot]
c12a972abd chore(cargo): bump brotli from 6.0.0 to 7.0.0
Bumps [brotli](https://github.com/dropbox/rust-brotli) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:37:58 +00:00
dependabot[bot]
145b91c2de chore(cargo): bump hyper from 1.4.1 to 1.5.0
Bumps [hyper](https://github.com/hyperium/hyper) from 1.4.1 to 1.5.0.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.4.1...v1.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
a49c25bbee chore(cargo): bump kamadak-exif from 0.5.5 to 0.6.0
Bumps [kamadak-exif](https://github.com/kamadak/exif-rs) from 0.5.5 to 0.6.0.
- [Changelog](https://github.com/kamadak/exif-rs/blob/master/NEWS)
- [Commits](https://github.com/kamadak/exif-rs/compare/0.5.5...0.6)

---
updated-dependencies:
- dependency-name: kamadak-exif
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
a439224f9e chore(cargo): bump once_cell from 1.19.0 to 1.20.2
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.19.0 to 1.20.2.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.19.0...v1.20.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
64cd7f8d31 chore(cargo): bump futures from 0.3.30 to 0.3.31
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.30 to 0.3.31.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.30...0.3.31)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
48ab5d4089 chore(cargo): bump rustls-pki-types from 1.9.0 to 1.10.0
Bumps [rustls-pki-types](https://github.com/rustls/pki-types) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.9.0...v/1.10.0)

---
updated-dependencies:
- dependency-name: rustls-pki-types
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
cd2394c31e chore(cargo): bump image from 0.25.2 to 0.25.4
Bumps [image](https://github.com/image-rs/image) from 0.25.2 to 0.25.4.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.2...v0.25.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
c972d7b6ef chore(cargo): bump typescript-type-def from 0.5.12 to 0.5.13
Bumps [typescript-type-def](https://github.com/dbeckwith/rust-typescript-type-def) from 0.5.12 to 0.5.13.
- [Changelog](https://github.com/dbeckwith/rust-typescript-type-def/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dbeckwith/rust-typescript-type-def/compare/v0.5.12...v0.5.13)

---
updated-dependencies:
- dependency-name: typescript-type-def
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
170023f1c8 chore(cargo): bump human-panic from 2.0.1 to 2.0.2
Bumps [human-panic](https://github.com/rust-cli/human-panic) from 2.0.1 to 2.0.2.
- [Changelog](https://github.com/rust-cli/human-panic/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/human-panic/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: human-panic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
5dc746d691 chore(cargo): bump serde_json from 1.0.128 to 1.0.132
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.128 to 1.0.132.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/1.0.128...1.0.132)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
dependabot[bot]
91acf0708a chore(cargo): bump anyhow from 1.0.89 to 1.0.92
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.89 to 1.0.92.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.89...1.0.92)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 16:36:30 +00:00
77 changed files with 1746 additions and 1219 deletions

22
.github/workflows/nix.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Test Nix flake
on:
pull_request:
push:
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

View File

@@ -1,5 +1,43 @@
# Changelog
## [1.149.0] - 2024-11-05
### Build system
- Update tokio to 1.41 and Android NDK to r27.
- `nix flake update android`.
### Fixes
- cargo: Update iroh to 0.28.1.
This fixes the problem with iroh not sending the `Host:` header and not being able to connect to relays behind nginx reverse proxy.
## [1.148.7] - 2024-11-03
### API-Changes
- Add API to reset contact encryption.
### Features / Changes
- Emit chatlist events only if message still exists.
### Fixes
- send_msg_to_smtp: Do not fail if the message does not exist anymore.
- Do not percent-encode dot when passing to autoconfig server.
- Save contact name from SecureJoin QR to `authname`, not to `name` ([#6115](https://github.com/deltachat/deltachat-core-rust/pull/6115)).
- Always exit fake IDLE after at most 60 seconds.
- Concat NDNs ([#6129](https://github.com/deltachat/deltachat-core-rust/pull/6129)).
### Refactor
- Remove `has_decrypted_pgp_armor()`.
### Miscellaneous Tasks
- Update dependencies.
## [1.148.6] - 2024-10-31
### API-Changes
@@ -4443,14 +4481,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
@@ -5193,3 +5227,5 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.148.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.3..v1.148.4
[1.148.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.4..v1.148.5
[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

585
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.148.6"
version = "1.149.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -46,7 +46,7 @@ async-native-tls = { version = "0.5", default-features = false, features = ["run
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
brotli = { version = "6", default-features=false, features = ["std"] }
brotli = { version = "7", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
@@ -62,10 +62,10 @@ http-body-util = "0.1.2"
humansize = "2"
hyper = "1"
hyper-util = "0.1.10"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.26.0", default-features = false, features = ["net"] }
iroh-net = { version = "0.26.0", default-features = false }
kamadak-exif = "0.5.3"
image = { version = "0.25.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
iroh-net = { version = "0.28.1", default-features = false }
kamadak-exif = "0.6.0"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
mailparse = "0.15"
@@ -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"
@@ -85,7 +85,7 @@ rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.9.0"
rustls-pki-types = "1.10.0"
rustls = { version = "0.23.14", default-features = false }
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
@@ -171,13 +171,13 @@ chrono = { version = "0.4.38", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
futures = "0.3.30"
futures = "0.3.31"
futures-lite = "2.4.0"
libc = "0.2"
log = "0.4"
nu-ansi-term = "0.46"
num-traits = "0.2"
once_cell = "1.18.0"
once_cell = "1.20.2"
rand = "0.8"
regex = "1.10"
rusqlite = "0.32"
@@ -186,14 +186,7 @@ serde = "1.0"
serde_json = "1"
tempfile = "3.13.0"
thiserror = "1"
# 1.38 is the latest version before `mio` dependency update
# that broke compilation with Android NDK r23c and r24.
# Version 1.39.0 cannot be compiled using these NDKs,
# see issue <https://github.com/tokio-rs/tokio/issues/6748>
# for details.
tokio = "~1.38.1"
tokio = "1"
tokio-util = "0.7.11"
tracing-subscriber = "0.3"
yerpc = "0.6.2"

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,

View File

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

View File

@@ -506,6 +506,11 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
* This is an experimental option not compatible to other MUAs
* and older Delta Chat versions.
* 1 = enable.
* 0 = disable (default).
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
@@ -2536,6 +2541,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
@@ -5704,8 +5711,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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.148.6"
version = "1.149.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -25,7 +25,7 @@ async-channel = { workspace = true }
futures = { workspace = true }
serde_json = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.12", features = ["json_value"] }
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
tokio = { workspace = true }
sanitize-filename = { workspace = true }
walkdir = "2.5.0"

View File

@@ -4,8 +4,8 @@ This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) inte
The JSON-RPC API is exposed in two fashions:
- A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
- The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
@@ -24,17 +24,16 @@ If you want to use the server in a production setup, first build it in release m
```sh
cargo build --features webserver --release
```
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
The server can be configured with environment variables:
| variable | default | description |
| ------------------ | ------------ | ------------------------- |
| `DC_PORT` | `20808` | port to listen on |
| `DC_ACCOUNTS_PATH` | `./accounts` | path to storage directory |
|variable|default|description|
|-|-|-|
|`DC_PORT`|`20808`|port to listen on|
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
If you are targeting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
@@ -44,53 +43,30 @@ cross build --features=webserver --target armv7-linux-androideabi --release
#### Using the TypeScript/JavaScript client
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
```sh
cd typescript
npm install
npm run build
```
The package is also published on npm under the name [`@deltachat/jsonrpc-client`](https://www.npmjs.com/package/@deltachat/jsonrpc-client).
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
###### Usage
Stdio server (recommended):
```typescript
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
import { C } from "@deltachat/jsonrpc-client";
const dc = await startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
const accounts = await dc.rpc.getAllAccounts();
console.log("accounts", accounts);
dc.close();
```
Websocket:
```typescript
import { WebsocketDeltaChat as DeltaChat } from '@deltachat/jsonrpc-client''=
import { DeltaChat } from './deltachat.bundle.js'
const dc = new DeltaChat('ws://localhost:20808/ws')
console.log(await dc.rpc.getSystemInfo());
const accounts = await dc.rpc.getAllAccounts()
console.log('accounts', accounts)
```
##### Generate TypeScript/JavaScript documentation
A script is included to build autogenerated documentation, which includes all RPC methods:
```sh
cd typescript
npm run docs
```
Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
## Development
@@ -105,7 +81,6 @@ npm run build
npm run example:build
npm run example:start
```
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
Run `npm run example:dev` to live-rebuild the example app when files changes.
@@ -129,7 +104,7 @@ cd typescript
npm run test
```
This will build the `deltachat-rpc-server` binary and then run a test suite against the deltachat-rpc-server (stdio).
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).

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:#}"),
},
@@ -1419,6 +1424,15 @@ impl CommandApi {
Ok(())
}
/// Resets contact encryption.
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
contact_id.reset_encryption(&ctx).await?;
Ok(())
}
async fn change_contact_name(
&self,
account_id: u32,
@@ -1990,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)
}
@@ -2161,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))
}

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),
};

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))
}
}

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 {

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;

View File

@@ -1,188 +0,0 @@
# @deltachat/jsonrpc-client
This package is a client for the jsonrpc server.
> If you are looking for the functions in the documentation, they are under [`RawClient`](https://js.jsonrpc.delta.chat/classes/RawClient.html).
### Important Terms
- [delta chat core](https://github.com/deltachat/deltachat-core-rust/) the heart of all Delta Chat clients. Handels all the heavy lifting (email, encryption, ...) and provides an easy api for bots and clients (`getChatlist`, `getChat`, `getContact`, ...).
- [jsonrpc](https://www.jsonrpc.org/specification) is a json based protocol
for applications to speak to each other by [remote procedure calls](https://en.wikipedia.org/wiki/Remote_procedure_call) (short RPC),
which basically means that the client can call methods on the server by sending a json messages.
- [`deltachat-rpc-server`](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server) provides the jsonrpc api over stdio (stdin/stdout)
- [`@deltachat/stdio-rpc-server`](https://www.npmjs.com/package/@deltachat/stdio-rpc-server) is an easy way to install `deltachat-rpc-server` from npm and use it from nodejs.
#### Transport
You need to connect this client to an instance of deltachat core via a transport.
Currently there are 2 transports available:
- (recomended) `StdioTransport` usable from `StdioDeltaChat` - speak to `deltachat-rpc-server` directly
- `WebsocketTransport` usable from `WebsocketDeltaChat`
You can also make your own transport, for example deltachat desktop uses a custom transport that sends the json messages over electron ipc.
Just implement your transport based on the `Transport` interface - look at how the [stdio transport is implemented](https://github.com/deltachat/deltachat-core-rust/blob/7121675d226e69fd85d0194d4b9c4442e4dd8299/deltachat-jsonrpc/typescript/src/client.ts#L113) for an example, it's not hard.
## Usage
> The **minimum** nodejs version for `@deltachat/stdio-rpc-server` is `16`
```
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
```
```js
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
// Import constants you might need later
import { C } from "@deltachat/jsonrpc-client";
async function main() {
const dc = await startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
dc.close();
}
main();
```
For a more complete example refer to <https://github.com/deltachat-bot/echo/tree/master/nodejs_stdio_jsonrpc>.
### Listening for events
```ts
dc.on("Info", (accountId, { msg }) =>
console.info(accountId, "[core:info]", msg)
);
// Or get an event emitter for only one account
const emitter = dc.getContextEvents(accountId);
emitter.on("IncomingMsg", async ({ chatId, msgId }) => {
const message = await dc.rpc.getMessage(accountId, msgId);
console.log("got message in chat " + chatId + " : ", message.text);
});
```
### Getting Started
This section describes how to handle the Delta Chat core library over the jsonrpc bindings.
For general information about Delta Chat itself,
see <https://delta.chat> and <https://github.com/deltachat>.
Let's start.
First of all, you have to start the deltachat-rpc-server process.
```js
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
const dc = await startDeltaChat("deltachat-data");
```
Then we have to create an Account (also called Context or profile) that is bound to a database.
The database is a normal SQLite file with a "blob directory" beside it.
But these details are handled by deltachat's account manager.
So you just have to tell the account manager to create a new account:
```js
const accountId = await dc.rpc.addAccount();
```
After that, register event listeners so you can see what core is doing:
Intenally `@deltachat/jsonrpc-client` implments a loop that waits for new events and then emits them to javascript land.
```js
dc.on("Info", (accountId, { msg }) =>
console.info(accountId, "[core:info]", msg)
);
```
Now you can **configure the account:**
```js
// use some real test credentials here
await dc.rpc.setConfig(accountId, "addr", "alice@example.org")
await dc.rpc.setConfig(accountId, "mail_pw", "***")
// you can also set multiple config options in one call
await dc.rpc.batchSetConfig(accountId, {
"addr": "alice@example.org",
"mail_pw": "***"
})
// after setting the credentials attempt to login
await dc.rpc.configure(accountId)
```
`configure()` returns a promise that is rejected on error (with await is is thrown).
The configuration itself may take a while. You can monitor it's progress like this:
```js
dc.on("ConfigureProgress", (accountId, { progress, comment }) => {
console.log(accountId, "ConfigureProgress", progress, comment);
});
// make sure to register this event handler before calling `dc.rpc.configure()`
```
The configuration result is saved in the database.
On subsequent starts it is not needed to call `dc.rpc.configure(accountId)`
(you can check this using `dc.rpc.isConfigured(accountId)`).
On a successfully configuration delta chat core automatically connects to the server, however subsequent starts you **need to do that manually** by calling `dc.rpc.startIo(accountId)` or `dc.rpc.startIoForAllAccounts()`.
```js
if (!await dc.rpc.isConfigured(accountId)) {
// use some real test credentials here
await dc.rpc.batchSetConfig(accountId, {
"addr": "alice@example.org",
"mail_pw": "***"
})
await dc.rpc.configure(accountId)
} else {
await dc.rpc.startIo(accountId)
}
```
Now you can **send the first message:**
```js
const contactId = await dc.rpc.createContact(accountId, "bob@example.org", null /* optional name */)
const chatId = await dc.rpc.createChatByContactId(accountId, contactId)
await dc.rpc.miscSendTextMessage(accountId, chatId, "Hi, here is my first message!")
```
`dc.rpc.miscSendTextMessage()` returns immediately;
the sending itself is done in the background.
If you check the testing address (bob),
you should receive a normal e-mail.
Answer this e-mail in any e-mail program with "Got it!",
and the IO you started above will **receive the message**.
You can then **list all messages** of a chat as follows:
```js
let i = 0;
for (const msgId of await exp.rpc.getMessageIds(120, 12, false, false)) {
i++;
console.log(`Message: ${i}`, (await dc.rpc.getMessage(120, msgId)).text);
}
```
This will output the following two lines:
```
Message 1: Hi, here is my first message!
Message 2: Got it!
```
<!-- TODO: ### Clean shutdown? - seems to be more advanced to call async functions on exit, also is this needed in this usecase? -->
## Further information
- `@deltachat/stdio-rpc-server`
- [package on npm](https://www.npmjs.com/package/@deltachat/stdio-rpc-server)
- [source code on github](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server/npm-package)
- [use `@deltachat/stdio-rpc-server` on an usuported platform](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server/npm-package#how-to-use-on-an-unsupported-platform)
- The issue-tracker for the core library is here: <https://github.com/deltachat/deltachat-core-rust/issues>
If you need further assistance,
please do not hesitate to contact us
through the channels shown at https://delta.chat/en/contribute
Please keep in mind, that your derived work
must respect the Mozilla Public License 2.0 of deltachat-rpc-server
and the respective licenses of the libraries deltachat-rpc-server links with.

View File

@@ -1,4 +1,4 @@
import { DcEvent, WebsocketDeltaChat as DeltaChat } from "../deltachat.js";
import { DcEvent, DeltaChat } from "../deltachat.js";
var SELECTED_ACCOUNT = 0;

View File

@@ -58,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.148.6"
"version": "1.149.0"
}

View File

@@ -5,14 +5,14 @@ import { RawClient } from "../generated/client.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
export type Events = { ALL: (accountId: number, event: EventType) => void } & {
type Events = { ALL: (accountId: number, event: EventType) => void } & {
[Property in EventType["kind"]]: (
accountId: number,
event: Extract<EventType, { kind: Property }>
) => void;
};
export type ContextEvents = { ALL: (event: EventType) => void } & {
type ContextEvents = { ALL: (event: EventType) => void } & {
[Property in EventType["kind"]]: (
event: Extract<EventType, { kind: Property }>
) => void;
@@ -83,7 +83,7 @@ export const DEFAULT_OPTS: Opts = {
url: "ws://localhost:20808/ws",
startEventLoop: true,
};
export class WebsocketDeltaChat extends BaseDeltaChat<WebsocketTransport> {
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
opts: Opts;
close() {
this.transport.close();

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.148.6"
version = "1.149.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -36,6 +36,10 @@ class Contact:
"""Delete contact."""
self._rpc.delete_contact(self.account.id, self.id)
def reset_encryption(self) -> None:
"""Reset contact encryption."""
self._rpc.reset_contact_encryption(self.account.id, self.id)
def set_name(self, name: str) -> None:
"""Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name)

View File

@@ -7,8 +7,8 @@ If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import logging
import os
import sys
import threading
import time
@@ -25,9 +25,7 @@ def path_to_webxdc(request):
def log(msg):
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
logging.info(msg)
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):

View File

@@ -57,8 +57,8 @@ def test_acfactory(acfactory) -> None:
if event.progress == 1000: # Success
break
else:
print(event)
print("Successful configuration")
logging.info(event)
logging.info("Successful configuration")
def test_configure_starttls(acfactory) -> None:
@@ -246,6 +246,7 @@ def test_contact(acfactory) -> None:
assert repr(alice_contact_bob)
alice_contact_bob.block()
alice_contact_bob.unblock()
alice_contact_bob.reset_encryption()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()

View File

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

View File

@@ -18,46 +18,20 @@ import { startDeltaChat } from "@deltachat/stdio-rpc-server";
import { C } from "@deltachat/jsonrpc-client";
async function main() {
const dc = await startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
dc.close();
const dc = await startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
dc.close()
}
main();
main()
```
For a more complete example refer to https://github.com/deltachat-bot/echo/tree/master/nodejs_stdio_jsonrpc.
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
## How to use on an unsupported platform
You need to have rust installed to compile deltachat core for your platform and cpu architecture.
<https://rustup.rs/> is the recommended way to install rust.
Also your system probably needs more than 4gb ram to compile core, alternatively your could try to build the debug build, that might take less ram to build.
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
1. clone the core repo, right next to your project folder: `git clone git@github.com:deltachat/deltachat-core-rust.git`
2. go into your core checkout and run `git pull` and `git checkout <version>` to point it to the correct version (needs to be the same version the `@deltachat/jsonrpc-client` package has)
3. run `cargo build --release --package deltachat-rpc-server --bin deltachat-rpc-server`
Then you have 2 options:
### point to deltachat-rpc-server via direct path:
```sh
# start your app with the DELTA_CHAT_RPC_SERVER env var
DELTA_CHAT_RPC_SERVER="../deltachat-core-rust/target/release/deltachat-rpc-server" node myapp.js
```
### install deltachat-rpc-server in your $PATH:
```sh
# use this to install to ~/.cargo/bin
cargo install --release --package deltachat-rpc-server --bin deltachat-rpc-server
# or manually move deltachat-core-rust/target/release/deltachat-rpc-server
# to a location that is included in your $PATH Environment variable.
```
```js
startDeltaChat("data-dir", { takeVersionFromPATH: true });
```
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
## How does it work when you install it
@@ -72,7 +46,7 @@ references:
When you import this package it searches for the rpc server in the following locations and order:
1. `DELTA_CHAT_RPC_SERVER` environment variable
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
3. prebuilds in npm packages
so by default it uses the prebuilds.

View File

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

View File

@@ -11,6 +11,13 @@ ignore = [
# Unmaintained encoding
"RUSTSEC-2021-0153",
# Unmaintained proc-macro-error
# <https://rustsec.org/advisories/RUSTSEC-2024-0370>
"RUSTSEC-2024-0370",
# Unmaintained instant
"RUSTSEC-2024-0384",
]
[bans]
@@ -26,13 +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 = "h2", version = "0.3.26" },
{ name = "http-body", version = "0.4.6" },
{ name = "http", version = "0.2.12" },
{ name = "hyper", version = "0.14.28" },
{ 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" },
@@ -43,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" },
@@ -55,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" },
]

111
flake.lock generated
View File

@@ -3,15 +3,15 @@
"android": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1712088936,
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
"lastModified": 1731356359,
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
"type": "github"
},
"original": {
@@ -22,18 +22,17 @@
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"android",
"nixpkgs"
]
},
"locked": {
"lastModified": 1711099426,
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"owner": "numtide",
"repo": "devshell",
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"type": "github"
},
"original": {
@@ -48,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": {
@@ -66,11 +65,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
@@ -84,29 +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",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
@@ -120,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": {
@@ -135,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": {
@@ -150,11 +131,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
@@ -166,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": {
@@ -182,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": {
@@ -195,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": {
@@ -213,7 +193,7 @@
"inputs": {
"android": "android",
"fenix": "fenix",
"flake-utils": "flake-utils_3",
"flake-utils": "flake-utils_2",
"naersk": "naersk",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_4"
@@ -222,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": {
@@ -265,21 +245,6 @@
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -18,9 +18,9 @@
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
inherit (sdkPkgs) ndk-27-0-11902837 cmdline-tools-latest;
});
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.0.11902837";
rustSrc = nix-filter.lib {
root = ./.;
@@ -257,13 +257,21 @@
androidAttrs = {
armeabi-v7a = {
cc = "armv7a-linux-androideabi19-clang";
cc = "armv7a-linux-androideabi21-clang";
rustTarget = "armv7-linux-androideabi";
};
arm64-v8a = {
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
x86 = {
cc = "i686-linux-android21-clang";
rustTarget = "i686-linux-android";
};
x86_64 = {
cc = "x86_64-linux-android21-clang";
rustTarget = "x86_64-linux-android";
};
};
mkAndroidRustPackage = arch: packageName:
@@ -525,28 +533,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
];
};
}
);
}

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.
///

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,

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,

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.148.6"
"version": "1.149.0"
}

View File

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

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()

View File

@@ -1 +1 @@
2024-10-31
2024-11-05

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;
@@ -520,8 +519,13 @@ Authentication-Results: dkim=";
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
// Test that Autocrypt works with mailing list.
//
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
// This is not needed: comparing of the From address to Autocrypt header address is enough.
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -533,28 +537,18 @@ Authentication-Results: dkim=";
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_none());
// Do the same without the mailing list header, this time the peerstate should be accepted
let sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_some());
// This also means that Bob can now write encrypted to Alice:
// Bob can now write encrypted to Alice:
let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await;
assert!(sent.load_from_db().await.get_showpadlock());
// But if Bob writes to a mailing list, Alice doesn't show a padlock
// since she can't verify the signature without accepting Bob's key:
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(!rcvd.get_showpadlock());
assert!(rcvd.get_showpadlock());
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(())

View File

@@ -1,11 +1,11 @@
//! # Chat module.
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
use std::{cmp, convert};
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress};
@@ -809,6 +809,14 @@ impl ChatId {
///
/// Passing `None` as message just deletes the draft
pub async fn set_draft(self, context: &Context, mut msg: Option<&mut Message>) -> Result<()> {
let self_in_chat = self.is_self_in_chat(context).await?;
if !self_in_chat {
warn!(
context,
"Not setting draft because self is not member of the chat"
);
return Ok(());
}
if self.is_special() {
return Ok(());
}
@@ -875,6 +883,22 @@ impl ChatId {
> 0)
}
async fn get_chat_type(self, context: &Context) -> Result<Chattype> {
context
.sql
.query_get_value("SELECT type FROM chats WHERE id=?;", (self,))
.await
.map(|res| res.ok_or(anyhow!("Can't get type for char")))
.and_then(convert::identity)
}
async fn is_self_in_chat(self, context: &Context) -> Result<bool> {
match self.get_chat_type(context).await? {
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
Chattype::Group => is_contact_in_chat(context, self, ContactId::SELF).await,
}
}
/// Set provided message as draft message for specified chat.
/// Returns true if the draft was added or updated in place.
async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<bool> {
@@ -922,24 +946,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);
}
}
}
@@ -1835,8 +1862,9 @@ impl Chat {
/// This is somewhat experimental, even more so than the rest of
/// deltachat, and the data returned is still subject to change.
pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
let self_in_chat = self.is_self_in_chat(context).await?;
let draft = match self.id.get_draft(context).await? {
Some(message) => message.text,
Some(message) if self_in_chat => message.text,
_ => String::new(),
};
Ok(ChatInfo {
@@ -3481,29 +3509,6 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
Ok(list)
}
/// Returns a vector of contact IDs for given chat ID where the contact is not SELF.
pub async fn get_other_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
// groupchat but the chats stays visible, moreover, this makes displaying lists easier)
let list = context
.sql
.query_map(
"SELECT cc.contact_id
FROM chats_contacts cc
LEFT JOIN contacts c
ON c.id=cc.contact_id
WHERE cc.chat_id=? AND c.id!=1
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
(chat_id,),
|row| row.get::<_, ContactId>(0),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
Ok(list)
}
/// Creates a group chat with a given `name`.
pub async fn create_group_chat(
context: &Context,
@@ -4528,6 +4533,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
/// Adds an informational message to chat.
///
/// For example, it can be a message showing that a member was added to a group.
/// Doesn't fail if the chat doesn't exist.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add_info_msg_with_cmd(
context: &Context,
@@ -4913,6 +4919,70 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_draft_only_in_accesible_chat_summary() -> Result<()> {
let mut t = TestContextManager::new();
let alice = t.alice().await;
let bob = t.bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "chat").await?;
let bob_contact = Contact::create(&alice, "Bob", "bob@example.net").await?;
add_contact_to_chat(&alice, chat_id, bob_contact).await?;
let bob_chat_id = bob
.recv_msg(&alice.send_text(chat_id, "hello bob").await)
.await
.chat_id;
bob_chat_id.accept(&bob).await?;
// Set draft and assure it is in chat info.
let draft = String::from("I'm gonna send this!!");
bob_chat_id
.set_draft(&bob, Some(&mut Message::new_text(draft.clone())))
.await?;
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
let info = chat.get_info(&bob).await?;
assert_eq!(info.draft, draft);
// Alice removes bob, so draft is not shown in chat info.
remove_contact_from_chat(&alice, chat_id, bob_contact).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert!(!chat.can_send(&bob).await?);
let info = chat.get_info(&bob).await?;
assert_eq!(info.draft, String::from(""));
// Alice re-adds bob, so draft is shown again.
add_contact_to_chat(&alice, chat_id, bob_contact).await?;
let bob_chat_id = bob
.recv_msg(&alice.send_text(chat_id, "hello again, bob").await)
.await
.chat_id;
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert!(chat.can_send(&bob).await?);
let info = chat.get_info(&bob).await?;
assert_eq!(info.draft, draft);
// Bob leaves group so draft is not shown.
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert!(!chat.can_send(&bob).await?);
let info = chat.get_info(&bob).await?;
assert_eq!(info.draft, String::from(""));
// Bob can not set a draft if not in chat.
bob_chat_id
.set_draft(
&bob,
Some(&mut Message::new_text(String::from(
"I'm gonna send this fr fr!!",
))),
)
.await?;
assert_eq!(bob_chat_id.get_draft(&bob).await?.unwrap().text, draft);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -5393,7 +5463,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(())
}
@@ -7718,4 +7788,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(())
}
}

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;

View File

@@ -396,6 +396,12 @@ pub enum Config {
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Enable header protection for `Autocrypt` header.
///
/// This is an experimental setting not compatible to other MUAs
/// and older Delta Chat versions (core version <= v1.149.0).
ProtectAutocrypt,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]

View File

@@ -611,8 +611,6 @@ pub enum Error {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::login_param::EnteredServerLoginParam;

View File

@@ -272,8 +272,6 @@ pub(crate) async fn moz_autoconfigure(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]

View File

@@ -215,8 +215,6 @@ pub(crate) async fn outlk_autodiscover(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]

View File

@@ -143,6 +143,43 @@ impl ContactId {
.await?;
Ok(())
}
/// Returns contact adress.
pub async fn addr(&self, context: &Context) -> Result<String> {
let addr = context
.sql
.query_row("SELECT addr FROM contacts WHERE id=?", (self,), |row| {
let addr: String = row.get(0)?;
Ok(addr)
})
.await?;
Ok(addr)
}
/// Resets encryption with the contact.
///
/// Effect is similar to receiving a message without Autocrypt header
/// from the contact, but this action is triggered manually by the user.
///
/// For example, this will result in sending the next message
/// to 1:1 chat unencrypted, but will not remove existing verified keys.
pub async fn reset_encryption(self, context: &Context) -> Result<()> {
let now = time();
let addr = self.addr(context).await?;
if let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? {
peerstate.degrade_encryption(now);
peerstate.save_to_db(&context.sql).await?;
}
// Reset 1:1 chat protection.
if let Some(chat_id) = ChatId::lookup_by_contact(context, self).await? {
chat_id
.set_protection(context, ProtectionStatus::Unprotected, now, Some(self))
.await?;
}
Ok(())
}
}
impl fmt::Display for ContactId {
@@ -1215,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, "");
@@ -3152,4 +3189,59 @@ Until the false-positive is fixed:
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reset_encryption() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
assert_eq!(msg.get_showpadlock(), false);
let msg = tcm.send_recv(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reset_verified_encryption() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.execute_securejoin(bob, alice).await;
let msg = tcm.send_recv(bob, alice, "Encrypted").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
// Check that the contact is still verified after resetting encryption.
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?;
assert_eq!(alice_bob_contact.is_verified(alice).await?, true);
// 1:1 chat and profile is no longer verified.
assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false);
let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await;
assert_eq!(
info_msg.text,
"bob@example.net sent a message from another device."
);
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
}
}

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};
@@ -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}>"),
};
@@ -990,6 +999,12 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"protect_autocrypt",
self.get_config_int(Config::ProtectAutocrypt)
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
@@ -1171,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
@@ -2057,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(())
}
}

View File

@@ -1,125 +1,36 @@
//! End-to-end decryption support.
use std::collections::HashSet;
use std::str::FromStr;
use anyhow::Result;
use deltachat_contact_tools::addr_cmp;
use mailparse::ParsedMail;
use crate::aheader::Aheader;
use crate::authres::handle_authres;
use crate::authres::{self, DkimResults};
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::peerstate::Peerstate;
use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
///
/// If successful and the message is encrypted, returns decrypted body and a set of valid
/// signature fingerprints.
///
/// If the message is wrongly signed, HashSet will be empty.
/// If successful and the message is encrypted, returns decrypted body.
pub fn try_decrypt(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
) -> Result<Option<::pgp::composed::Message>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
}
let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::pk_decrypt(data, private_keyring)?;
pub(crate) async fn prepare_decryption(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DecryptionInfo> {
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
info!(
context,
"Ignoring autocrypt header since this is a mailing list message. \
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
);
}
return Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header: None,
peerstate: None,
message_time,
dkim_results: DkimResults { dkim_passed: false },
});
}
let autocrypt_header = if context.is_self_addr(from).await? {
None
} else if let Some(aheader_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
} else {
None
};
let dkim_results = handle_authres(context, mail, from).await?;
let allow_aeap = get_encrypted_mime(mail).is_some();
let peerstate = get_autocrypt_peerstate(
context,
from,
autocrypt_header.as_ref(),
message_time,
allow_aeap,
)
.await?;
Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header,
peerstate,
message_time,
dkim_results,
})
}
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
pub from: String,
pub autocrypt_header: Option<Aheader>,
/// The peerstate that will be used to validate the signatures
pub peerstate: Option<Peerstate>,
/// The timestamp when the message was sent.
/// If this is older than the peerstate's last_seen, this probably
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
pub(crate) dkim_results: authres::DkimResults,
Ok(Some(msg))
}
/// Returns a reference to the encrypted payload of a message.
fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
pub(crate) fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
@@ -204,37 +115,6 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
}
}
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let data = mail.get_body_raw()?;
if has_decrypted_pgp_armor(&data) {
let (plain, ret_valid_signatures) =
pgp::pk_decrypt(data, private_keyring, public_keyring_for_validate)?;
return Ok(Some((plain, ret_valid_signatures)));
}
Ok(None)
}
#[allow(clippy::indexing_slicing)]
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
if let Some(index) = input.iter().position(|b| *b > b' ') {
if input.len() - index > 26 {
let start = index;
let end = start + 27;
return &input[start..end] == b"-----BEGIN PGP MESSAGE-----";
}
}
false
}
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
///
/// Returns the signed part and the set of key
@@ -302,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?;
@@ -346,24 +226,6 @@ mod tests {
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" \n-----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" -----BEGIN PGP MESSAGE---";
assert_eq!(has_decrypted_pgp_armor(data), false);
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b"blas";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted

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,

View File

@@ -144,12 +144,12 @@ impl HtmlMsgParser {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().to_ascii_lowercase() == "flowed"
format.as_str().eq_ignore_ascii_case("flowed")
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
delsp.as_str().eq_ignore_ascii_case("yes")
} else {
false
},
@@ -283,7 +283,6 @@ mod tests {
<meta name="color-scheme" content="light dark" />
</head><body>
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"#
);
@@ -302,7 +301,6 @@ This message does not have Content-Type nor Subject.<br/>
<meta name="color-scheme" content="light dark" />
</head><body>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<br/>
</body></html>
"#
);
@@ -325,7 +323,6 @@ This line ends with a space and will be merged with the next one due to format=f
<br/>
This line does not end with a space<br/>
and will be wrapped as usual.<br/>
<br/>
</body></html>
"#
);
@@ -347,7 +344,6 @@ mime-modified should not be set set as there is no html and no special stuff;<br
although not being a delta-message.<br/>
test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/>
<br/>
<br/>
</body></html>
"#
);

View File

@@ -89,7 +89,7 @@ pub(crate) struct Imap {
oauth2: bool,
login_failed_once: bool,
authentication_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
@@ -254,7 +254,7 @@ impl Imap {
proxy_config,
strict_tls,
oauth2,
login_failed_once: false,
authentication_failed_once: false,
connectivity: Default::default(),
conn_last_try: UNIX_EPOCH,
conn_backoff_ms: 0,
@@ -402,7 +402,7 @@ impl Imap {
let mut lock = context.server_id.write().await;
lock.clone_from(&session.capabilities.server_id);
self.login_failed_once = false;
self.authentication_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
lp.user
@@ -416,35 +416,38 @@ impl Imap {
let imap_user = lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
let err_str = err.to_string();
warn!(context, "IMAP failed to login: {err:#}.");
first_error.get_or_insert(format_err!("{message} ({err:#})"));
// If it looks like the password is wrong, send a notification:
let _lock = context.wrong_pw_warning_mutex.lock().await;
if !configuring
&& self.login_failed_once
&& err_str.to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
let mut msg = Message::new_text(message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
.await
if err.to_string().to_lowercase().contains("authentication") {
if self.authentication_failed_once
&& !configuring
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
warn!(context, "Failed to add device message: {e:#}.");
let mut msg = Message::new_text(message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
.await
{
warn!(context, "Failed to add device message: {e:#}.");
} else {
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
.log_err(context)
.ok();
}
} else {
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
.log_err(context)
.ok();
self.authentication_failed_once = true;
}
} else {
self.login_failed_once = true;
self.authentication_failed_once = false;
}
}
}
@@ -1913,7 +1916,7 @@ async fn needs_move_to_mvbox(
&& has_chat_version
&& headers
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.to_ascii_lowercase() == "auto-generated")
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
.is_some()
{
if let Some(from) = mimeparser::get_from(headers) {

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;
@@ -750,7 +750,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 +878,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();

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")

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,

View File

@@ -880,8 +880,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;

View File

@@ -150,16 +150,17 @@ impl MsgId {
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
update_msg_state(context, self, MessageState::OutDelivered).await?;
let chat_id: ChatId = context
let chat_id: Option<ChatId> = context
.sql
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,))
.await?
.unwrap_or_default();
.await?;
context.emit_event(EventType::MsgDelivered {
chat_id,
chat_id: chat_id.unwrap_or_default(),
msg_id: self,
});
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if let Some(chat_id) = chat_id {
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}
Ok(())
}
@@ -1850,20 +1851,21 @@ pub(crate) async fn set_msg_failed(
}
msg.error = Some(error.to_string());
context
let exists = context
.sql
.execute(
"UPDATE msgs SET state=?, error=? WHERE id=?;",
(msg.state, error, msg.id),
)
.await?;
.await?
> 0;
context.emit_event(EventType::MsgFailed {
chat_id: msg.chat_id,
msg_id: msg.id,
});
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
if exists {
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
}
Ok(())
}

View File

@@ -743,7 +743,9 @@ impl MimeFactory {
hidden_headers.push(header);
} else if header_name == "chat-user-avatar" {
hidden_headers.push(header);
} else if header_name == "autocrypt" {
} else if header_name == "autocrypt"
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
{
unprotected_headers.push(header.clone());
} else if header_name == "from" {
// Unencrypted securejoin messages should _not_ include the display name:
@@ -2272,7 +2274,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()));

View File

@@ -4,6 +4,7 @@ use std::cmp::min;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::str;
use std::str::FromStr;
use anyhow::{bail, Context as _, Result};
use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
@@ -14,6 +15,7 @@ use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, Si
use rand::distributions::{Alphanumeric, DistString};
use crate::aheader::{Aheader, EncryptPreference};
use crate::authres::handle_authres;
use crate::blob::BlobObject;
use crate::chat::{add_info_msg, ChatId};
use crate::config::Config;
@@ -21,8 +23,8 @@ use crate::constants::{self, Chattype};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::decrypt::{
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature,
DecryptionInfo,
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
validate_detached_signature,
};
use crate::dehtml::dehtml;
use crate::events::EventType;
@@ -71,7 +73,8 @@ pub(crate) struct MimeMessage {
/// messages to this address to post them to the list.
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decryption_info: DecryptionInfo,
pub autocrypt_header: Option<Aheader>,
pub peerstate: Option<Peerstate>,
pub decrypting_failed: bool,
/// Set of valid signature fingerprints if a message is an
@@ -301,42 +304,101 @@ impl MimeMessage {
let mut from = from.context("No from in message")?;
let private_keyring = load_self_secret_keyring(context).await?;
let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, timestamp_sent).await?;
let allow_aeap = get_encrypted_mime(&mail).is_some();
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossiped_keys = Default::default();
let mut from_is_signed = false;
hop_info += "\n\n";
hop_info += &decryption_info.dkim_results.to_string();
hop_info += &dkim_results.to_string();
let incoming = !context.is_self_addr(&from.addr).await?;
let public_keyring = match decryption_info.peerstate.is_none() && !incoming {
true => key::load_self_public_keyring(context).await?,
false => keyring_from_peerstate(decryption_info.peerstate.as_ref()),
};
let (mail, mut signatures, encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &public_keyring)
}) {
Ok(Some((raw, signatures))) => {
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
let mail_raw; // Memory location for a possible decrypted message.
let decrypted_msg; // Decrypted signed OpenPGP message.
let (mail, encrypted) =
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
Ok(Some(msg)) => {
mail_raw = msg.get_content()?.unwrap_or_default();
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
decrypted_msg = Some(msg);
if let Some(protected_aheader_value) = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
{
aheader_value = Some(protected_aheader_value);
}
(Ok(decrypted_mail), true)
}
Ok(None) => {
mail_raw = Vec::new();
decrypted_msg = None;
(Ok(mail), false)
}
Err(err) => {
mail_raw = Vec::new();
decrypted_msg = None;
warn!(context, "decryption failed: {:#}", err);
(Err(err), false)
}
};
let autocrypt_header = if !incoming {
None
} else if let Some(aheader_value) = aheader_value {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
(Ok(decrypted_mail), signatures, true)
}
Ok(None) => (Ok(mail), HashSet::new(), false),
Err(err) => {
warn!(context, "decryption failed: {:#}", err);
(Err(err), HashSet::new(), false)
}
} else {
None
};
// The peerstate that will be used to validate the signatures.
let mut peerstate = get_autocrypt_peerstate(
context,
&from.addr,
autocrypt_header.as_ref(),
timestamp_sent,
allow_aeap,
)
.await?;
let public_keyring = match peerstate.is_none() && !incoming {
true => key::load_self_public_keyring(context).await?,
false => keyring_from_peerstate(peerstate.as_ref()),
};
let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)?
} else {
HashSet::new()
};
let mail = mail.as_ref().map(|mail| {
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
.unwrap_or((mail, Default::default()));
@@ -422,7 +484,7 @@ impl MimeMessage {
Self::remove_secured_headers(&mut headers);
// If it is not a read receipt, degrade encryption.
if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, mail) {
if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
if timestamp_sent > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report"
{
@@ -433,7 +495,7 @@ impl MimeMessage {
if !encrypted {
signatures.clear();
}
if let Some(peerstate) = &mut decryption_info.peerstate {
if let Some(peerstate) = &mut peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await?;
@@ -449,7 +511,8 @@ impl MimeMessage {
from_is_signed,
incoming,
chat_disposition_notification_to,
decryption_info,
autocrypt_header,
peerstate,
decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message
@@ -602,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
@@ -617,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];
}
}
@@ -770,15 +835,6 @@ impl MimeMessage {
Ok(())
}
/// Set different sender name for a message.
/// This overrides the name set by the `set_config()`-option `displayname`.
pub fn set_override_sender_name(&mut self, name: Option<String>) {
self.parts.iter_mut().for_each(|part| {
part.param
.set_optional(Param::OverrideSenderDisplayname, name.clone());
});
}
async fn avatar_action_from_header(
&mut self,
context: &Context,
@@ -1167,7 +1223,7 @@ impl MimeMessage {
let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
{
format.as_str().to_ascii_lowercase() == "flowed"
format.as_str().eq_ignore_ascii_case("flowed")
} else {
false
};
@@ -1177,7 +1233,7 @@ impl MimeMessage {
&& is_format_flowed
{
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
delsp.as_str().eq_ignore_ascii_case("yes")
} else {
false
};
@@ -1240,7 +1296,7 @@ impl MimeMessage {
if decoded_data.is_empty() {
return Ok(());
}
if let Some(peerstate) = &mut self.decryption_info.peerstate {
if let Some(peerstate) = &mut self.peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual
&& mime_type.type_() == mime::APPLICATION
&& mime_type.subtype().as_str() == "pgp-keys"
@@ -1697,7 +1753,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();
@@ -1860,18 +1915,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);
}
@@ -2308,8 +2362,6 @@ async fn ndn_maybe_add_info_msg(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use mailparse::ParsedMail;
use super::*;
@@ -3654,6 +3706,28 @@ On 2020-10-25, Bob wrote:
Ok(())
}
/// Tests that sender status (signature) does not appear
/// in HTML view of a long message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_large_message_no_signature() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config(Config::Selfstatus, Some("Some signature"))
.await?;
let chat = alice.create_chat(bob).await;
let txt = "Hello!\n".repeat(500);
let sent = alice.send_text(chat.id, &txt).await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.has_html(), true);
let html = msg.id.get_html(bob).await?.unwrap();
assert_eq!(html.contains("Some signature"), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_x_microsoft_original_message_id() {
let t = TestContext::new_alice().await;
@@ -4021,12 +4095,8 @@ Content-Disposition: reaction\n\
// We do allow the time to be in the future a bit (because of unsynchronized clocks),
// but only 60 seconds:
assert!(mime_message.decryption_info.message_time <= time() + 60);
assert!(mime_message.decryption_info.message_time >= beginning_time + 60);
assert_eq!(
mime_message.decryption_info.message_time,
mime_message.timestamp_sent
);
assert!(mime_message.timestamp_sent <= time() + 60);
assert!(mime_message.timestamp_sent >= beginning_time + 60);
assert!(mime_message.timestamp_rcvd <= time());
Ok(())
@@ -4097,4 +4167,24 @@ Content-Type: text/plain; charset=utf-8
"alice@example.org"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protect_autocrypt() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config_bool(Config::ProtectAutocrypt, true)
.await?;
bob.set_config_bool(Config::ProtectAutocrypt, true).await?;
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
assert_eq!(msg.get_showpadlock(), false);
let msg = tcm.send_recv(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
Ok(())
}
}

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)

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());

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);
@@ -766,8 +766,7 @@ pub(crate) async fn maybe_do_aeap_transition(
context: &Context,
mime_parser: &mut crate::mimeparser::MimeMessage,
) -> Result<()> {
let info = &mime_parser.decryption_info;
let Some(peerstate) = &info.peerstate else {
let Some(peerstate) = &mime_parser.peerstate else {
return Ok(());
};
@@ -815,13 +814,13 @@ pub(crate) async fn maybe_do_aeap_transition(
// DC avoids sending messages with the same timestamp, that's why messages
// with equal timestamps are ignored here unlike in `Peerstate::apply_header()`.
if info.message_time <= peerstate.last_seen {
if mime_parser.timestamp_sent <= peerstate.last_seen {
info!(
context,
"Not doing AEAP from {} to {} because {} < {}.",
&peerstate.addr,
&mime_parser.from.addr,
info.message_time,
mime_parser.timestamp_sent,
peerstate.last_seen
);
return Ok(());
@@ -832,24 +831,23 @@ pub(crate) async fn maybe_do_aeap_transition(
"Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
);
let info = &mut mime_parser.decryption_info;
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
let peerstate = mime_parser.peerstate.as_mut().context("no peerstate??")?;
// Add info messages to chats with this (verified) contact
//
peerstate
.handle_setup_change(
context,
info.message_time,
PeerstateChange::Aeap(info.from.clone()),
mime_parser.timestamp_sent,
PeerstateChange::Aeap(mime_parser.from.addr.clone()),
)
.await?;
let old_addr = mem::take(&mut peerstate.addr);
peerstate.addr.clone_from(&info.from);
let header = info.autocrypt_header.as_ref().context(
peerstate.addr.clone_from(&mime_parser.from.addr);
let header = mime_parser.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(context, header, info.message_time);
peerstate.apply_header(context, header, mime_parser.timestamp_sent);
peerstate
.save_to_db_ex(&context.sql, Some(&old_addr))
@@ -890,12 +888,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,
@@ -915,7 +913,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");
@@ -934,7 +932,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,
@@ -971,7 +969,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,

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,
@@ -297,43 +332,43 @@ pub fn pk_calc_signature(
///
/// Receiver private keys are provided in
/// `private_keys_for_decryption`.
///
/// Returns decrypted message and fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
#[allow(clippy::implicit_hasher)]
pub fn pk_decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
) -> Result<pgp::composed::Message> {
let cursor = Cursor::new(ctext);
let (msg, _) = Message::from_armor_single(cursor)?;
let (msg, _headers) = Message::from_armor_single(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let (msg, _) = msg.decrypt(|| "".into(), &skeys[..])?;
let (msg, _key_ids) = msg.decrypt(|| "".into(), &skeys[..])?;
// get_content() will decompress the message if needed,
// but this avoids decompressing it again to check signatures
let msg = msg.decompress()?;
let content = match msg.get_content()? {
Some(content) => content,
None => bail!("The decrypted message is empty"),
};
Ok(msg)
}
/// Returns fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
///
/// If the message is wrongly signed, HashSet will be empty.
pub fn valid_signature_fingerprints(
msg: &pgp::composed::Message,
public_keys_for_validation: &[SignedPublicKey],
) -> Result<HashSet<Fingerprint>> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
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);
}
}
}
Ok((content, ret_signature_fingerprints))
Ok(ret_signature_fingerprints)
}
/// Validates detached signature.
@@ -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())?;
@@ -407,6 +446,18 @@ mod tests {
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
fn pk_decrypt_and_validate(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(pgp::composed::Message, HashSet<Fingerprint>)> {
let msg = pk_decrypt(ctext, private_keys_for_decryption)?;
let ret_signature_fingerprints =
valid_signature_fingerprints(&msg, public_keys_for_validation)?;
Ok((msg, ret_signature_fingerprints))
}
#[test]
fn test_split_armored_data_1() {
let (typ, _headers, base64) = split_armored_data(
@@ -534,34 +585,35 @@ mod tests {
// Check decrypting as Alice
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_no_sig_check() {
let keyring = vec![KEYS.alice_secret.clone()];
let (plain, valid_signatures) =
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap();
assert_eq!(plain, CLEARTEXT);
let (msg, valid_signatures) =
pk_decrypt_and_validate(ctext_signed().await.as_bytes().to_vec(), &keyring, &[])
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -570,26 +622,26 @@ mod tests {
// The validation does not have the public key of the signer.
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.bob_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let (plain, valid_signatures) = pk_decrypt(
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring,
&[],
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
}

View File

@@ -2,7 +2,7 @@
use once_cell::sync::Lazy;
use crate::simplify::split_lines;
use crate::simplify::remove_message_footer;
/// Plaintext message body together with format=flowed attributes.
#[derive(Debug)]
@@ -32,7 +32,8 @@ impl PlainText {
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});
let lines = split_lines(&self.text);
let lines: Vec<&str> = self.text.lines().collect();
let (lines, _footer) = remove_message_footer(&lines);
let mut ret = r#"<!DOCTYPE html>
<html><head>
@@ -136,7 +137,28 @@ line 1<br/>
line 2<br/>
line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/>
<a href="http://link-at-start-of-line.org">http://link-at-start-of-line.org</a><br/>
<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_remove_signature() {
let html = PlainText {
text: "Foo\nbar\n-- \nSignature here".to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
Foo<br/>
bar<br/>
</body></html>
"#
);

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;

View File

@@ -367,9 +367,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 +547,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 +565,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 +638,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 +669,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 +802,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 +854,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 +870,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 +897,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 +908,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");
};
@@ -1309,7 +1302,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 +1333,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 {

View File

@@ -201,7 +201,7 @@ pub(crate) async fn receive_imf_inner(
};
crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?;
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
if let Some(peerstate) = &mime_parser.peerstate {
peerstate
.handle_fingerprint_change(context, mime_parser.timestamp_sent)
.await?;
@@ -356,8 +356,7 @@ pub(crate) async fn receive_imf_inner(
// Peerstate could be updated by handling the Securejoin handshake.
let contact = Contact::get_by_id(context, from_id).await?;
mime_parser.decryption_info.peerstate =
Peerstate::from_addr(context, contact.get_addr()).await?;
mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
} else {
let to_id = to_ids.first().copied().unwrap_or_default();
// handshake may mark contacts as verified and must be processed before chats are created
@@ -393,7 +392,7 @@ pub(crate) async fn receive_imf_inner(
if verified_encryption == VerifiedEncryption::Verified
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some()
{
if let Some(peerstate) = &mut mime_parser.decryption_info.peerstate {
if let Some(peerstate) = &mut mime_parser.peerstate {
// NOTE: it might be better to remember ID of the key
// that we used to decrypt the message, but
// it is unlikely that default key ever changes
@@ -1006,7 +1005,7 @@ async fn add_parts(
)
.await?;
}
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
if let Some(peerstate) = &mime_parser.peerstate {
restore_protection = new_protection != ProtectionStatus::Protected
&& peerstate.prefer_encrypt == EncryptPreference::Mutual
// Check that the contact still has the Autocrypt key same as the
@@ -1424,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)
}
}
}
}
@@ -1507,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)
@@ -1807,14 +1813,6 @@ async fn lookup_chat_by_reply(
// If this was a private message just to self, it was probably a private reply.
// It should not go into the group then, but into the private chat.
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
// If the parent chat is a 1:1 chat, then the message should go to the 1:1 chat.
if to_ids.len() == 1 {
let name = chat::get_other_chat_contacts(context, parent_chat_id).await?[0];
if from_id != *name {
mime_parser.set_override_sender_name(Some(name.get_stock_name(context).await));
}
return Ok(Some((parent_chat.id, parent_chat.blocked)));
}
return Ok(None);
}
@@ -2086,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,
@@ -2189,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())
@@ -2456,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(),
};
@@ -2510,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();
}
@@ -2670,7 +2684,7 @@ async fn update_verified_keys(
return Ok(None);
}
let Some(peerstate) = &mut mimeparser.decryption_info.peerstate else {
let Some(peerstate) = &mut mimeparser.peerstate else {
// No peerstate means no verified keys.
return Ok(None);
};
@@ -2743,7 +2757,7 @@ async fn has_verified_encryption(
// this check is skipped for SELF as there is no proper SELF-peerstate
// and results in group-splits otherwise.
if from_id != ContactId::SELF {
let Some(peerstate) = &mimeparser.decryption_info.peerstate else {
let Some(peerstate) = &mimeparser.peerstate else {
return Ok(NotVerified(
"No peerstate, the contact isn't verified".to_string(),
));

View File

@@ -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();

View File

@@ -135,7 +135,7 @@ impl SchedulerState {
/// If in the meantime [`SchedulerState::start`] or [`SchedulerState::stop`] is called
/// resume will do the right thing and restore the scheduler to the state requested by
/// the last call.
pub(crate) async fn pause<'a>(&'_ self, context: Context) -> Result<IoPausedGuard> {
pub(crate) async fn pause(&'_ self, context: Context) -> Result<IoPausedGuard> {
{
let mut inner = self.inner.write().await;
match *inner {

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);
}
};

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.
@@ -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()

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.

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,20 +21,20 @@ 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)]
fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<&'a [&'a str]>) {
pub(crate) fn remove_message_footer<'a>(
lines: &'a [&str],
) -> (&'a [&'a str], Option<&'a [&'a str]>) {
let mut nearly_standard_footer = None;
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);
}
@@ -42,7 +43,7 @@ fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<&'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)
}
@@ -51,7 +52,6 @@ fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<&'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 == "--"
@@ -61,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)
@@ -171,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;
@@ -186,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,
@@ -236,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))
@@ -296,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 {
@@ -401,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);

View File

@@ -357,9 +357,9 @@ pub(crate) async fn send_msg_to_smtp(
.await
.context("failed to update retries count")?;
let (body, recipients, msg_id, retries) = context
let Some((body, recipients, msg_id, retries)) = context
.sql
.query_row(
.query_row_optional(
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
(rowid,),
|row| {
@@ -370,7 +370,10 @@ pub(crate) async fn send_msg_to_smtp(
Ok((mime, recipients, msg_id, retries))
},
)
.await?;
.await?
else {
return Ok(());
};
if retries > 6 {
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.")

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
};

View File

@@ -542,7 +542,7 @@ impl ContactId {
}
/// Get contact name, e.g. `Bob`, or `bob@exmple.net` if no name is set.
pub async fn get_stock_name(self, context: &Context) -> String {
async fn get_stock_name(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_display_name().to_string())

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};

View File

@@ -1,5 +1,3 @@
#![allow(clippy::indexing_slicing)]
use anyhow::Result;
use crate::chat;

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::*;