Compare commits

..

111 Commits

Author SHA1 Message Date
Septias
9754299ca8 make it build 2024-11-13 14:11:34 +01:00
Septias
113cae3b85 Merge branch 'sk/fmt_flake' into sk/json_rpc_darwin 2024-11-13 14:11:33 +01:00
Septias
3c3995af79 Add darwin build targets 2024-11-13 14:11:17 +01:00
Septias
4b13f9352c fmt 2024-11-13 13:35:17 +01:00
Septias
37f15c5282 fmt flake with alejandra 2024-11-13 13:35:17 +01: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
Sebastian Klähn
dd73d23a0a fix: concat ndns (#6129)
close #2338

Concat error messages when receiving new ndns.
This PR adds a newline followed by the new NDN error to the error text.
Maybe we should use something more prominent like
```
-----------------------------------------------------------------------
```
or more newlines, but I'm not sure. This maybe has to be tested on a
real device to see what works best.
2024-11-02 08:20:27 +00:00
dependabot[bot]
3292ba260d chore(cargo): bump futures-lite from 2.3.0 to 2.4.0
Bumps [futures-lite](https://github.com/smol-rs/futures-lite) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/smol-rs/futures-lite/releases)
- [Changelog](https://github.com/smol-rs/futures-lite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/futures-lite/compare/v2.3.0...v2.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:44:40 +00:00
dependabot[bot]
5fe42f193e chore(cargo): bump uuid from 1.10.0 to 1.11.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.10.0...1.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:43:53 +00:00
dependabot[bot]
af42abd0aa chore(cargo): bump thiserror from 1.0.64 to 1.0.66
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.64 to 1.0.66.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.64...1.0.66)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:43:29 +00:00
dependabot[bot]
c8803f6f05 chore(cargo): bump hyper-util from 0.1.9 to 0.1.10
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.9 to 0.1.10.
- [Release notes](https://github.com/hyperium/hyper-util/releases)
- [Changelog](https://github.com/hyperium/hyper-util/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper-util/compare/v0.1.9...v0.1.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:43:09 +00:00
dependabot[bot]
3ad83ade12 chore(cargo): bump bytes from 1.7.2 to 1.8.0
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.7.2 to 1.8.0.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.7.2...v1.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:42:45 +00:00
dependabot[bot]
d9ce231199 chore(cargo): bump async-smtp from 0.9.1 to 0.9.2
Bumps [async-smtp](https://github.com/async-email/async-smtp) from 0.9.1 to 0.9.2.
- [Commits](https://github.com/async-email/async-smtp/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:42:28 +00:00
dependabot[bot]
0a3787c389 chore(cargo): bump quick-xml from 0.36.2 to 0.37.0
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.36.2 to 0.37.0.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.36.2...v0.37.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:41:34 +00:00
dependabot[bot]
8a278c3ee9 chore(cargo): bump rustls from 0.23.13 to 0.23.14
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.13 to 0.23.14.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.13...v/0.23.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:40:59 +00:00
dependabot[bot]
3129e20726 chore(cargo): bump pin-project from 1.1.5 to 1.1.7
Bumps [pin-project](https://github.com/taiki-e/pin-project) from 1.1.5 to 1.1.7.
- [Release notes](https://github.com/taiki-e/pin-project/releases)
- [Changelog](https://github.com/taiki-e/pin-project/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/pin-project/compare/v1.1.5...v1.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 03:40:10 +00:00
link2xt
4ee65a049f fix: always exit fake IDLE after at most 60 seconds
Do not call `fetch_new_messages`,
always exit and let the IMAP loop
prepare the connection properly and run all pending tasks.
2024-11-01 21:28:22 +00:00
iequidoo
bea7e4792c fix: Save contact name from SecureJoin QR to authname, not to name (#6115)
3f9242a saves name from all QR codes to `name` (i.e. manually edited name), but for SecureJoin QR
codes the name should be saved to `authname` because such QR codes are generated by the
inviter. Other QR codes may be generated locally and not only by Delta Chat, so the name from them
mustn't go to `authname` and be revealed to the network or other contacts.
2024-11-01 12:34:24 -03:00
link2xt
ded8c02c0f fix(send_msg_to_smtp): do not fail if the message does not exist anymore
If the number of retries for message is exceeded,
do not fail when marking it as failed if the message does not exist.
Otherwise we may never delete the message from SMTP queue
because corresponding msg_id is not valid anymore.
2024-11-01 13:39:24 +00:00
link2xt
cbca5101b1 fix: do not percent-encode dot when passing to autoconfig server
The server should decode the URL and according to RFC 3986
query parameters may or may not be URL-encoded,
but at some servers don't decode the dot correctly.

`@` is decoded correctly by autoconfig.murena.io
2024-11-01 00:27:22 +00:00
B. Petersen
88278fc826 chore(release): prepare for 1.148.6 2024-10-31 17:29:55 +01:00
Hocuri
d8f07b2c5f feat: Enable Webxdc realtime by default (#6125) 2024-10-31 13:33:14 +01:00
link2xt
4850e3696d chore(cargo): upgrade iroh to 0.26.0 2024-10-31 02:17:37 +00:00
Hocuri
d6c2c863b7 refactor: Use Message::new_text() more (#6127)
Follow-up to https://github.com/deltachat/deltachat-core-rust/pull/6123
2024-10-30 12:05:58 +00:00
WofWca
6abadac4bb api: add MessageSearchResult.chat_id (#6120) 2024-10-30 02:58:17 +00:00
l
55702e4985 fix: skip IDLE if we got unsolicited FETCH (#6130)
This may indicate that there was a new \Seen flag
that we don't want to skip.

Also don't drain unsolicited responses while scanning folders. Now we
only drain unsolicited responses right before IDLE and always redo the
whole fetch cycle if there have been some. Some message in the scanned
folder may not be fetched that would be previously fetched otherwise,
but it will be picked up on the next folder scan.
2024-10-30 02:38:15 +00:00
Sebastian Klähn
9cb60f5f49 refactor: directly use connectives (#6128)
Just a small refactoring. Instead of rebinding res all the time just use
`and` and `and_then`how they are inteded to be used. Improves code
readability imo.
2024-10-29 21:49:44 +00:00
Hocuri
bb8b262e68 chore: Silence another rust-analyzer false-positive (#6124)
Follow-up to #6077. Not sure why this error didn't show up in my
rust-analyzer until now.
2024-10-29 17:45:26 +01:00
Hocuri
69fbb98f3c api: Add Message::new_text() (#6123)
This adds a function to `Message`:

```rust
    pub fn new_text(text: String) -> Self {
        Message {
            viewtype: Viewtype::Text,
            text,
            ..Default::default()
        }
    }
```

I keep expecting that a function like this must exist and being
surprised that it doesn't.

Open question is whether it should be `pub` or `pub(crate)` - I made it
`pub` for now because it may be useful for others and we currently we
aren't thinking about the Rust API that much, anyway, but I can make it
`pub(crate)`, too (then it can't be used in deltachat-jsonrpc and
deltachat-repl).

I replaced some usages of Message::new(Viewtype::Text), but not all yet,
I'm going to do this in a follow-up, which will remove another around 65
LOC.
2024-10-29 16:22:52 +01:00
Hocuri
c98d3818d5 fix: Show root SMTP connection failure in connectivity view (#6121)
Right now, when there is an SMTP connection error, the connectivity view
will always show "Error: SMTP connection failure: SMTP failed to
connect".

Instead, I just used the same method that is used in imap connect()
already.
2024-10-29 13:55:15 +01:00
iequidoo
10aa308501 fix: Save full text to mime_headers for long outgoing messages (#6091)
0a63083df7 (fix: Shorten message text in locally sent messages too)
sets `msgs.mime_modified` for long outgoing messages, but forgets to save full message text.
2024-10-28 12:30:29 -03:00
link2xt
146bcfe455 chore(release): prepare for 1.148.5 2024-10-27 17:03:49 +00:00
link2xt
f57cdc3a2c Revert "build: nix flake update fenix"
This reverts commit aa3ef5011b.

This fixes `nix build .#deltachat-rpc-server-armeabi-v7a-android`.
2024-10-27 16:53:49 +00:00
link2xt
e11fddf9aa ci: take CHATMAIL_DOMAIN from variables instead of secrets 2024-10-26 16:44:28 +00:00
link2xt
f396ff4297 fix: do not lock the account manager for the whole duration of background_fetch 2024-10-26 16:38:43 +00:00
link2xt
51a1762228 fix: do not take write lock for maybe_network_lost() and set_push_device_token() 2024-10-26 16:38:43 +00:00
link2xt
69b4c0ccb4 refactor: factor out add_gossip_peer_from_header()
Also don't even add the peer to SQL if realtime is disabled.
2024-10-25 19:25:51 +00:00
iequidoo
3f1dfef0e7 feat: Auto-restore 1:1 chat protection after receiving old unverified message
I.e. add the "Messages are guaranteed to be end-to-end encrypted from now on." message and mark the
chat as protected again because no user action is required in this case. There are a couple of
problems though:
- If the program crashes earlier than the protection is restored, the chat remains
  protection-broken. But this problem already exists because `ChatId::set_protection()` is never
  retried.
- If multiple old unverified messages are received, protection messages added in between don't
  annihilate, so they clutter the chat.
2024-10-25 14:20:09 -03:00
iequidoo
c0f5771140 refactor: receive_imf::add_parts: Remove excessive from_id == ContactId::SELF checks
`mime_parser.incoming` is already here for this and is checked above.
2024-10-25 14:20:09 -03:00
iequidoo
33cae2815d fix: Set Config::NotifyAboutWrongPw before saving configuration (#5896)
Let's always set `Config::NotifyAboutWrongPw` before saving configuration, better if a wrong
password notification is shown once more than not shown at all. It shouldn't be a big problem
because reconfiguration is a manual action and isn't done frequently.

Also for the same reason reset `Config::NotifyAboutWrongPw` only after a successful addition of the
appropriate device message.
2024-10-25 13:14:37 -03:00
link2xt
fc2b111f5d chore(release): prepare for 1.148.4 2024-10-24 20:25:58 +00:00
link2xt
913d2c45b3 fix: do not wait for connections in maybe_add_gossip_peers()
join() method of Gossip [1]
waits for at least one connection
and this is not what we want
because it may block receive_imf()
forever if no connection arrives.

[1] https://docs.rs/iroh-gossip/0.25.0/iroh_gossip/net/struct.Gossip.html#method.join
2024-10-24 19:59:00 +00:00
link2xt
e32d676a08 fix: normalize proxy URLs before saving into proxy_url 2024-10-24 16:43:10 +00:00
Simon Laux
9812d5ba75 feat: jsonrpc: add private_tag to Account::Configured Object (#6107)
Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-10-24 16:00:27 +00:00
link2xt
bc7568e39b chore(release): prepare for 1.148.3 2024-10-24 14:08:59 +00:00
link2xt
11bf1c45d2 test: test that realtime advertisements work after chatting 2024-10-24 13:56:04 +00:00
link2xt
122c23ad4e api(deltachat-rpc-client): add EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED 2024-10-24 13:56:04 +00:00
link2xt
a0bde4699e fix: fix reception of realtime advertisements 2024-10-24 13:56:04 +00:00
link2xt
ac01a4a771 feat: allow sending realtime messages up to 128 KB in size
Previous default value was 4 KiB.
2024-10-24 13:55:28 +00:00
link2xt
51f2a8d59e refactor: generate topic inside create_iroh_header() 2024-10-23 22:33:09 +00:00
bjoern
f208c31cdf docs: fix DC_QR_PROXY docs (#6099) 2024-10-23 22:29:06 +02:00
link2xt
acd7a1d17e chore(release): prepare for 1.148.2 2024-10-23 17:52:24 +00:00
link2xt
db6d451c90 feat: add more logging for iroh initialization and peer addition 2024-10-23 17:48:33 +00:00
link2xt
4b3a6445fb fix: never initialize Iroh if realtime is disabled 2024-10-23 17:48:33 +00:00
link2xt
aa3ef5011b build: nix flake update fenix 2024-10-23 03:31:39 +00:00
link2xt
1d3072c287 build: nix flake update nixpkgs 2024-10-23 03:19:33 +00:00
link2xt
4fb59177fa chore(release): prepare for 1.148.1 2024-10-23 02:37:22 +00:00
link2xt
d841bcb41e Revert "build: nix flake update"
This reverts commit 6f22ce2722.
2024-10-23 02:07:22 +00:00
link2xt
d205bc410b chore(release): prepare for 1.148.0 2024-10-23 00:27:49 +00:00
bjoern
0d573ac037 feat: add delta chat logo to QR codes (#6093)
the chosen error correction allows tolerates about 15% "erroneous
codewords", the logo is of a similar size as the old avatars,
so it should be fine.
2024-10-23 01:43:50 +02:00
link2xt
a55e33fbc7 fix(sql): run PRAGMA incremental_vacuum on a write connection
Otherwise it always fails with SQLITE_READONLY:
```
WARNING src/sql.rs:769: Failed to run incremental vacuum: attempt to write a readonly database: Error code 8: Attempt to write a readonly database.
```
2024-10-22 23:23:57 +00:00
bjoern
839b0e94af api: create QR codes from any data (#6090)
this PR adds a function that can be used to create any QR code, in a raw
form.

this can be used to create add-contact as well as add-second-device QR
codes (eg. `dc_create_qr_svg(dc_get_securejoin_qr())`) - as well as for
other QR codes as proxies.

the disadvantage of the rich-formatted QR codes as created by
`dc_get_securejoin_qr_svg()` and `dc_backup_provider_get_qr_svg()` were:

- they do not look good and cannot interact with UI layout wise (but
also tapping eg. an address is not easily possible)
- esp. text really looks bad. even with
[some](e5dc8fe3d8)
[hacks](https://github.com/deltachat/deltachat-android/pull/2215) it
[stays buggy](https://github.com/deltachat/deltachat-ios/issues/2200);
the bugs mainly come from different SVG implementation, all need their
own quirks
- accessibility is probably bad as well

we thought that time, SVG is a great thing for QR codes, but apart from
basic geometrics, it is not.

so, we avoid text, this also means to avoid putting an avatar in the
middle of the QR code (we can put some generic symbol there, eg.
different ones for add-contact and add-second-device).

while this looks like a degradation, also other messengers use more raw
QR codes. also, we removed many data from the QR code anyway, eg. the
email address is no longer there. that time, sharing QR images was more
a thing, meanwhile we have invite links, that are much better for that
purpose.

in theory, we could also leave the SVG path completely and go for PNG -
which we did not that time as PNG and text looks bad, as the system font
is not easily usable :) but going for PNG would add further challenges
as passing binary data around, and also UI-implemtation-wise, that would
be a larger step. so, let's stay with SVG in a form we know is
compatible.

the old QR code functions are deprecated.
2024-10-22 21:49:45 +02:00
bjoern
f2e600dc55 feat: internal profile names (#6088)
this PR allows setting a "private tag" for a profile, see
https://github.com/deltachat/deltachat-android/pull/3373 for a possible
UI.

currently, the core does not do anything with the tag (so, it could also
be a ui.-config option), however, this may change in the future - it
might bet synced, and become also otherwise useful in core. also, having
this in core is better documentation-wise, as otherwise each UI easily
does its own things :)
2024-10-22 09:43:36 +02:00
bjoern
61fd0d400f notify adding reactions (#6072)
this PR adds an event for reactions received for one's own messages.

this will allow UIs to add notification for these reactions.

**Screenshots** at https://github.com/deltachat/deltachat-ios/pull/2331:

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-10-21 21:35:03 +02:00
link2xt
7424d06416 refactor(sql): move write mutex into connection pool 2024-10-21 19:14:37 +00:00
link2xt
aa71fbe04c refactor: resultify get_self_fingerprint() 2024-10-21 13:03:58 +00:00
link2xt
c5cadd9991 feat: add in-memory cache for DNS
This adds "stale-while-revalidate" in-memory cache for DNS. Instead of
calling `tokio::net::lookup_host` we use previous result of
`tokio::net::lookup_host` immediately and spawn revalidation task in the
background. This way all lookups after the first successful one return
immediately.

Most of the time results returned by resolvers are the same anyway, but
with this cache we avoid waiting 60 second timeout if DNS request is
lost. Common reason result may be different is round-robin DNS load
balancing and switching from IPv4 to IPv6 network. For round-robin DNS
we don't break load balancing but simply use a different result, and for
IPv6 we anyway likely have a result in persistent cache and can use IPv4
otherwise.

Especially frequent should be the case when you send a message over SMTP
and SMTP connection is stale (older than 60 s), so we open a new one.
With this change new connection will be set up faster as you don't need
to wait for DNS resolution, so message will be sent faster.
2024-10-21 10:46:11 +00:00
Septias
c92554dc1f fix typo 2024-10-21 11:29:55 +02:00
link2xt
94c6d1dea4 fix: call update_connection_history for proxified connections 2024-10-20 18:36:37 +00:00
Hocuri
d27d0ef476 chore: Silence a rust-analyzer false-positive (#6077)
rust-analyzer was showing warnings here because it is always also
building in the Test configuration, and EventType has a

```rust
#[cfg(test)]
Test,
```
variant, which was not matched.
2024-10-20 20:21:32 +02:00
Hocuri
d3f75360fa fix: Resolve warning about default-features, and make it possible to disable vendoring (#6079)
On main, when running `cargo build`, the following warning is emitted:

> warning:
/home/jonathan/deltachat-android/jni/deltachat-core-rust/deltachat-ffi/Cargo.toml:
`default-features` is ignored for deltachat, since `default-features`
was not specified for `workspace.dependencies.deltachat`, this could
become a hard error in the future

This is because when referring to a workspace dependency, it's not
possible to remove features, it's only possible to add features, so that
the `vendored` feature was always enabled with no possibility to disable
it.

This PR restores the wanted behavior of enabling vendoring by default
with the possibility to disable it with "default-features = false".

It fixes `nix build .#python-docs` by not passing
`--no-default-features` when building deltachat with nix.
2024-10-20 18:33:47 +02:00
link2xt
06a6cc48d2 feat(sql): set PRAGMA query_only to avoid writing on read-only connections
Co-authored-by: iequidoo <dgreshilov@gmail.com>
2024-10-20 14:51:46 +00:00
iequidoo
b13f2709be test: Message from old setup preserves contact verification, but breaks 1:1 protection
If a message from an old contact's setup is received, the outdated Autocrypt header isn't applied,
so the contact verification preserves. But the chat protection breaks because the old message is
sorted to the bottom as it mustn't be sorted over the protection info message (which is `InNoticed`
moreover). Would be nice to preserve the chat protection too e.g. add a "protection broken" message,
then the old message and then a new "protection enabled" message, but let's record the current
behaviour first.
2024-10-20 10:05:28 -03:00
95 changed files with 2967 additions and 1464 deletions

View File

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

View File

@@ -33,7 +33,7 @@ jobs:
working-directory: deltachat-jsonrpc/typescript
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
- name: make sure websocket server version still builds
working-directory: deltachat-jsonrpc
run: cargo build --bin deltachat-jsonrpc-server --features webserver

View File

@@ -64,5 +64,5 @@ jobs:
working-directory: node
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"

View File

@@ -1,5 +1,221 @@
# 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
- Add Message::new_text() ([#6123](https://github.com/deltachat/deltachat-core-rust/pull/6123)).
- Add `MessageSearchResult.chat_id` ([#6120](https://github.com/deltachat/deltachat-core-rust/pull/6120)).
### Features / Changes
- Enable Webxdc realtime by default ([#6125](https://github.com/deltachat/deltachat-core-rust/pull/6125)).
### Fixes
- Save full text to mime_headers for long outgoing messages ([#6091](https://github.com/deltachat/deltachat-core-rust/pull/6091)).
- Show root SMTP connection failure in connectivity view ([#6121](https://github.com/deltachat/deltachat-core-rust/pull/6121)).
- Skip IDLE if we got unsolicited FETCH ([#6130](https://github.com/deltachat/deltachat-core-rust/pull/6130)).
### Miscellaneous Tasks
- Silence another rust-analyzer false-positive ([#6124](https://github.com/deltachat/deltachat-core-rust/pull/6124)).
- cargo: Upgrade iroh to 0.26.0.
### Refactor
- Directly use connectives ([#6128](https://github.com/deltachat/deltachat-core-rust/pull/6128)).
- Use Message::new_text() more ([#6127](https://github.com/deltachat/deltachat-core-rust/pull/6127)).
## [1.148.5] - 2024-10-27
### Fixes
- Set Config::NotifyAboutWrongPw before saving configuration ([#5896](https://github.com/deltachat/deltachat-core-rust/pull/5896)).
- Do not take write lock for maybe_network_lost() and set_push_device_token().
- Do not lock the account manager for the whole duration of background_fetch.
### Features / Changes
- Auto-restore 1:1 chat protection after receiving old unverified message.
### CI
- Take `CHATMAIL_DOMAIN` from variables instead of secrets.
### Other
- Revert "build: nix flake update fenix" to fix `nix build .#deltachat-rpc-server-armeabi-v7a-android`.
### Refactor
- Receive_imf::add_parts: Remove excessive `from_id == ContactId::SELF` checks.
- Factor out `add_gossip_peer_from_header()`.
## [1.148.4] - 2024-10-24
### Features / Changes
- Jsonrpc: add `private_tag` to `Account::Configured` Object ([#6107](https://github.com/deltachat/deltachat-core-rust/pull/6107)).
### Fixes
- Normalize proxy URLs before saving into proxy_url.
- Do not wait for connections in maybe_add_gossip_peers().
## [1.148.3] - 2024-10-24
### Fixes
- Fix reception of realtime advertisements.
### Features / Changes
- Allow sending realtime messages up to 128 KB in size.
### API-Changes
- deltachat-rpc-client: Add EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED.
### Documentation
- Fix DC_QR_PROXY docs ([#6099](https://github.com/deltachat/deltachat-core-rust/pull/6099)).
### Refactor
- Generate topic inside create_iroh_header().
### Tests
- Test that realtime advertisements work after chatting.
## [1.148.2] - 2024-10-23
### Fixes
- Never initialize Iroh if realtime is disabled.
### Features / Changes
- Add more logging for iroh initialization and peer addition.
### Build system
- `nix flake update nixpkgs`.
- `nix flake update fenix`.
## [1.148.1] - 2024-10-23
### Build system
- Revert "build: nix flake update"
This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
## [1.148.0] - 2024-10-22
### API-Changes
- Create QR codes from any data ([#6090](https://github.com/deltachat/deltachat-core-rust/pull/6090)).
- Add delta chat logo to QR codes ([#6093](https://github.com/deltachat/deltachat-core-rust/pull/6093)).
- Add realtime advertisement received event ([#6043](https://github.com/deltachat/deltachat-core-rust/pull/6043)).
- Notify adding reactions ([#6072](https://github.com/deltachat/deltachat-core-rust/pull/6072))
- Internal profile names ([#6088](https://github.com/deltachat/deltachat-core-rust/pull/6088)).
### Features / Changes
- IMAP COMPRESS support.
- Sort received outgoing message down if it's fresher than all non fresh messages.
- Prioritize cached results if DNS resolver returns many results.
- Add in-memory cache for DNS.
- deltachat-repl: Built-in QR code printer.
- Log the logic for (not) doing AEAP.
- Log when late Autocrypt header is ignored.
- Add more context to `send_msg` errors.
### Fixes
- Replace old draft with a new one atomically.
- ChatId::maybe_delete_draft: Don't delete message if it's not a draft anymore ([#6053](https://github.com/deltachat/deltachat-core-rust/pull/6053)).
- Call update_connection_history for proxified connections.
- sql: Set PRAGMA query_only to avoid writing on read-only connections.
- sql: Run `PRAGMA incremental_vacuum` on a write connection.
- Increase MAX_SECONDS_TO_LEND_FROM_FUTURE to 30.
### Build system
- Nix flake update.
- Resolve warning about default-features, and make it possible to disable vendoring ([#6079](https://github.com/deltachat/deltachat-core-rust/pull/6079)).
- Silence a rust-analyzer false-positive ([#6077](https://github.com/deltachat/deltachat-core-rust/pull/6077)).
### CI
- Update Rust to 1.82.0.
### Documentation
- Set_protection_for_timestamp_sort does not send messages.
- Document MimeFactory.req_mdn.
- Fix `too_long_first_doc_paragraph` clippy lint.
### Refactor
- Update_msg_state: Don't avoid downgrading OutMdnRcvd to OutDelivered.
- Fix elided_named_lifetimes warning.
- set_protection_for_timestamp_sort: Do not log bubbled up errors.
- Fix clippy::needless_lifetimes warnings.
- Use `HeaderDef` constant for Chat-Disposition-Notification-To.
- Resultify get_self_fingerprint().
- sql: Move write mutex into connection pool.
### Tests
- test_qr_setup_contact_svg: Stop testing for no display name.
- Always gossip if gossip_period is set to 0.
- test_aeap_flow_verified: Wait for "member added" before sending messages ([#6057](https://github.com/deltachat/deltachat-core-rust/pull/6057)).
- Make test_verified_group_member_added_recovery more reliable.
- test_aeap_flow_verified: Do not start ac1new.
- Fix `test_securejoin_after_contact_resetup` flakiness.
- Message from old setup preserves contact verification, but breaks 1:1 protection.
## [1.147.1] - 2024-10-13
### Build system
@@ -4265,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
@@ -5008,3 +5220,12 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.146.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.145.0..v1.146.0
[1.147.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.146.0..v1.147.0
[1.147.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.0..v1.147.1
[1.148.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.1..v1.148.0
[1.148.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.0..v1.148.1
[1.148.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.1..v1.148.2
[1.148.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.2..v1.148.3
[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

View File

@@ -27,7 +27,7 @@ add_custom_command(
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --features jsonrpc
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)

687
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.147.1"
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" }
@@ -61,11 +61,11 @@ hickory-resolver = "=0.25.0-alpha.2"
http-body-util = "0.1.2"
humansize = "2"
hyper = "1"
hyper-util = "0.1.9"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.25.0", default-features = false, features = ["net"] }
iroh-net = { version = "0.25.0", default-features = false }
kamadak-exif = "0.5.3"
hyper-util = "0.1.10"
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"
@@ -79,14 +79,14 @@ percent-encoding = "2.3"
pgp = { version = "0.13.2", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.36"
quick-xml = "0.37"
quoted_printable = "0.5"
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.9.0"
rustls = { version = "0.23.13", default-features = false }
rustls-pki-types = "1.10.0"
rustls = { version = "0.23.14", default-features = false }
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
@@ -169,15 +169,15 @@ async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.38", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc" }
deltachat = { path = "." }
futures = "0.3.30"
futures-lite = "2.3.0"
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
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

@@ -0,0 +1,12 @@
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 24.015419,1.2870249 c -12.549421,0 -22.7283936,10.1789711 -22.7283936,22.7283931 0,12.549422 10.1789726,22.728395 22.7283936,22.728395 14.337742,-0.342877 9.614352,-4.702705 23.697556,0.969161 -7.545453,-13.001555 -1.082973,-13.32964 -0.969161,-23.697556 0,-12.549422 -10.178973,-22.7283931 -22.728395,-22.7283931 z" />
<path
style="fill:#000000;fill-opacity:1;stroke:none"
d="M 23.982249,5.3106163 C 13.645822,5.4364005 5.2618355,13.92999 5.2618355,24.275753 c 0,10.345764 8.3839865,18.635301 18.7204135,18.509516 9.827724,-0.03951 7.516769,-5.489695 18.380082,-0.443187 -5.950849,-9.296115 0.201753,-10.533667 0.340336,-18.521947 0,-10.345766 -8.383989,-18.6353031 -18.720418,-18.5095187 z" />
<g
style="fill:#ffffff"
transform="scale(1.1342891,0.88160947)">
<path
d="m 21.360141,23.513382 q -1.218487,-1.364705 -3.387392,-3.265543 -2.388233,-2.095797 -3.216804,-3.289913 -0.828571,-1.218486 -0.828571,-2.6563 0,-2.144536 1.998318,-3.363022 1.998317,-1.2428565 5.215121,-1.2428565 3.216804,0 5.605037,1.0966375 2.412603,1.096638 2.412603,3.021846 0,0.92605 -0.584873,1.535293 -0.584874,0.609243 -1.364705,0.609243 -1.121008,0 -2.631931,-1.681511 -1.535292,-1.705881 -2.60756,-2.388233 -1.047898,-0.706722 -2.461343,-0.706722 -1.803359,0 -2.973106,0.804201 -1.145377,0.804201 -1.145377,2.047057 0,1.169747 0.950419,2.193275 0.950419,1.023529 4.898315,3.728568 4.215963,2.899998 5.946213,4.532769 1.75462,1.632772 2.851258,3.972265 1.096638,2.339494 1.096638,4.947055 0,4.581508 -3.241174,8.090749 -3.216804,3.484871 -7.530245,3.484871 -3.923526,0 -6.628566,-2.802519 -2.705039,-2.802518 -2.705039,-7.481506 0,-4.508399 2.973106,-7.530245 2.997477,-3.021846 7.359658,-3.655459 z m 1.072268,1.121008 q -6.994112,1.145377 -6.994112,9.601672 0,4.36218 1.730251,6.774783 1.75462,2.412603 4.069744,2.412603 2.412603,0 3.972265,-2.315124 1.559663,-2.339493 1.559663,-6.311759 0,-5.751255 -4.337811,-10.162175 z" />
</g>

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.147.1"
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.
@@ -522,14 +527,16 @@ char* dc_get_blobdir (const dc_context_t* context);
* In contrast to `dc_set_chat_mute_duration()`,
* fresh message and badge counters are not changed by this setting,
* but should be tuned down where appropriate.
* - `private_tag` = Optional tag as "Work", "Family".
* Meant to help profile owner to differ between profiles with similar names.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
* however, are not handled by the core otherwise.
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
* 1 = WebXDC realtime API is enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop.
* 1 = WebXDC realtime API is enabled (default).
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -2531,8 +2538,8 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_SOCKS5_PROXY with dc_lot_t::text1=host, dc_lot_t::text2=port:
* ask the user if they want to use the given proxy and overwrite the previous one, if any.
* - 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.
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
@@ -2609,6 +2616,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
* See dc_get_securejoin_qr() for details about the contained QR code.
*
* @deprecated 2024-10 use dc_create_qr_svg(dc_get_securejoin_qr()) instead.
* @memberof dc_context_t
* @param context The context object.
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
@@ -2789,6 +2797,22 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
void dc_delete_all_locations (dc_context_t* context);
// misc
/**
* Create a QR code from any input data.
*
* The QR code is returned as a square SVG image.
*
* @memberof dc_context_t
* @param payload The content for the QR code.
* @return SVG image with the QR code.
* On errors, an empty string is returned.
* The returned string must be released using dc_str_unref() after usage.
*/
char* dc_create_qr_svg (const char* payload);
/**
* Get last error string.
*
@@ -2877,6 +2901,7 @@ char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider);
* This works like dc_backup_provider_qr() but returns the text of a rendered
* SVG image containing the QR code.
*
* @deprecated 2024-10 use dc_create_qr_svg(dc_backup_provider_get_qr()) instead.
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
@@ -2916,7 +2941,7 @@ void dc_backup_provider_unref (dc_backup_provider_t* backup_provider);
* Gets a backup offered by a dc_backup_provider_t object on another device.
*
* This function is called on a device that scanned the QR code offered by
* dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a
* dc_backup_provider_get_qr(). Typically this is a
* different device than that which provides the backup.
*
* This call will block while the backup is being transferred and only
@@ -6031,6 +6056,21 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_REACTIONS_CHANGED 2001
/**
* A reaction to one's own sent message received.
* Typically, the UI will show a notification for that.
*
* In addition to this event, DC_EVENT_REACTIONS_CHANGED is emitted.
*
* @param data1 (int) contact_id ID of the contact sending this reaction.
* @param data2 (int) msg_id + (char*) reaction.
* ID of the message for which a reaction was received in dc_event_get_data2_int(),
* and the reaction as dc_event_get_data2_str().
* string must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_INCOMING_REACTION 2002
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.

View File

@@ -30,7 +30,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
@@ -541,6 +541,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ErrorSelfNotInGroup(_) => 410,
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingReaction { .. } => 2002,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
@@ -568,6 +569,9 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
EventType::EventChannelOverflow { .. } => 2400,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -598,6 +602,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone => 0,
EventType::ChatlistChanged => 0,
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -628,6 +633,9 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
chat_id.unwrap_or_default().to_u32() as libc::c_int
}
EventType::EventChannelOverflow { n } => *n as libc::c_int,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -672,6 +680,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::EventChannelOverflow { .. } => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -685,6 +694,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
..
} => status_update_serial.to_u32() as libc::c_int,
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -758,6 +770,14 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
ptr as *mut libc::c_char
}
EventType::IncomingReaction { reaction, .. } => reaction
.as_str()
.to_c_string()
.unwrap_or_default()
.into_raw(),
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -2574,6 +2594,18 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
if payload.is_null() {
eprintln!("ignoring careless call to dc_create_qr_svg()");
return "".strdup();
}
create_qr_svg(&to_string_lossy(payload))
.unwrap_or_else(|_| "".to_string())
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
@@ -4836,7 +4868,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
}
let accounts = &*accounts;
block_on(async move { accounts.write().await.maybe_network_lost().await });
block_on(async move { accounts.read().await.maybe_network_lost().await });
}
#[no_mangle]
@@ -4850,12 +4882,12 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
}
let accounts = &*accounts;
block_on(async move {
let accounts = accounts.read().await;
accounts
.background_fetch(Duration::from_secs(timeout_in_seconds))
.await;
});
let background_fetch_future = {
let lock = block_on(accounts.read());
lock.background_fetch(Duration::from_secs(timeout_in_seconds))
};
// At this point account manager is not locked anymore.
block_on(background_fetch_future);
1
}
@@ -4873,7 +4905,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
let token = to_string_lossy(token);
block_on(async move {
let mut accounts = accounts.write().await;
let accounts = accounts.read().await;
if let Err(err) = accounts.set_push_device_token(&token).await {
accounts.emit_event(EventType::Error(format!(
"Failed to set notify token: {err:#}."

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.147.1"
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

@@ -254,11 +254,12 @@ impl CommandApi {
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
self.accounts
.write()
.await
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
.await;
let future = {
let lock = self.accounts.read().await;
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
};
// At this point account manager is not locked anymore.
future.await;
Ok(())
}
@@ -1418,6 +1419,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,
@@ -2117,8 +2127,7 @@ impl CommandApi {
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
let mut msg = Message::new_text(text);
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())

View File

@@ -17,6 +17,9 @@ pub enum Account {
// size: u32,
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
color: String,
/// Optional tag as "Work", "Family".
/// Meant to help profile owner to differ between profiles with similar names.
private_tag: Option<String>,
},
#[serde(rename_all = "camelCase")]
Unconfigured { id: u32 },
@@ -31,12 +34,14 @@ impl Account {
let color = color_int_to_hex_string(
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
);
let private_tag = ctx.get_config(Config::PrivateTag).await?;
Ok(Account::Configured {
id,
display_name,
addr,
profile_image,
color,
private_tag,
})
} else {
Ok(Account::Unconfigured { id })

View File

@@ -98,6 +98,14 @@ pub enum EventType {
contact_id: u32,
},
/// Incoming reaction, should be notified.
#[serde(rename_all = "camelCase")]
IncomingReaction {
contact_id: u32,
msg_id: u32,
reaction: String,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -302,6 +310,15 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
CoreEventType::IncomingReaction {
contact_id,
msg_id,
reaction,
} => IncomingReaction {
contact_id: contact_id.to_u32(),
msg_id: msg_id.to_u32(),
reaction: reaction.as_str().to_string(),
},
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
@@ -392,6 +409,9 @@ impl From<CoreEventType> for EventType {
},
CoreEventType::ChatlistChanged => ChatlistChanged,
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
}

View File

@@ -490,6 +490,7 @@ pub struct MessageSearchResult {
author_name: String,
author_color: String,
author_id: u32,
chat_id: u32,
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
@@ -529,6 +530,7 @@ impl MessageSearchResult {
author_name,
author_color: color_int_to_hex_string(sender.get_color()),
author_id: sender.id.to_u32(),
chat_id: chat.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,

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

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

View File

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

View File

@@ -22,6 +22,7 @@ use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::qr_code_generator::create_qr_svg;
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
@@ -425,6 +426,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
checkqr <qr-content>\n\
joinqr <qr-content>\n\
setqr <qr-content>\n\
createqrsvg <qr-content>\n\
providerinfo <addr>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
@@ -1002,8 +1004,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text);
draft.set_text(arg1.to_string());
let mut draft = Message::new_text(arg1.to_string());
sel_chat
.as_ref()
.unwrap()
@@ -1026,8 +1027,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
!arg1.is_empty(),
"Please specify text to add as device message."
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(arg1.to_string());
let mut msg = Message::new_text(arg1.to_string());
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
@@ -1249,6 +1249,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Err(err) => println!("Cannot set config from QR code: {err:?}"),
}
}
"createqrsvg" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let svg = create_qr_svg(arg1)?;
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
fs::write(&file, svg).await?;
println!("{file:#?} written.");
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
let proxy_enabled = context

View File

@@ -240,12 +240,13 @@ const CONTACT_COMMANDS: [&str; 9] = [
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 11] = [
const MISC_COMMANDS: [&str; 12] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
"createqrsvg",
"fileinfo",
"clear",
"exit",

View File

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

View File

@@ -63,6 +63,7 @@ class EventType(str, Enum):
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
class ChatId(IntEnum):

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,7 +7,8 @@ If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import sys
import logging
import os
import threading
import time
@@ -24,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):
@@ -107,13 +106,15 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
assert snapshot.text == "ping2"
log("sending realtime data ac1 -> ac2")
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
# Test that 128 KB of data can be sent in a single message.
data = os.urandom(128000)
ac1_webxdc_msg.send_webxdc_realtime_data(data)
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == list(b"foo")
assert event.data == list(data)
break
@@ -208,3 +209,28 @@ def test_no_reordering(acfactory, path_to_webxdc):
if event.data[0] == i:
break
pytest.fail("Reordering detected")
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
"""Test that realtime advertisement is assigned to the correct message after chatting."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
ac1_ac2_chat.send_text("Hello!")
ac2_hello_msg = ac2.wait_for_incoming_msg()
ac2_hello_msg_snapshot = ac2_hello_msg.get_snapshot()
assert ac2_hello_msg_snapshot.text == "Hello!"
ac2_hello_msg_snapshot.chat.accept()
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
while 1:
event = ac1.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
assert event.msg_id == ac1_webxdc_msg.id
break

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.147.1"
version = "1.149.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "1.147.1"
"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]
@@ -28,10 +35,7 @@ skip = [
{ name = "fastrand", version = "1.9.0" },
{ 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 = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },

52
flake.lock generated
View File

@@ -7,11 +7,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1729369131,
"narHash": "sha256-PtfScp+nQd1PsT5rf0Qgjdbsh4Iag6R1ivYMWLizyIc=",
"lastModified": 1731356359,
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "82bffbf3f06bdccf44fc62a9bd4f152ac80a55b0",
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1729375822,
"narHash": "sha256-bRo4xVwUhvJ4Gz+OhWMREFMdBOYSw4Yi1Apj01ebbug=",
"lastModified": 1731393059,
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
"owner": "nix-community",
"repo": "fenix",
"rev": "2853e7d9b5c52a148a9fb824bfe4f9f433f557ab",
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
"type": "github"
},
"original": {
@@ -116,11 +116,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"lastModified": 1730207686,
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4",
"type": "github"
},
"original": {
@@ -131,11 +131,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1729256560,
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1729070438,
"narHash": "sha256-KOTTUfPkugH52avUvXGxvWy8ibKKj4genodIYUED+Kc=",
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5785b6bb5eaae44e627d541023034e1601455827",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
@@ -163,12 +163,10 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1729265718,
"narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ccc0c2126893dd20963580b6478d1a10a4512185",
"type": "github"
"lastModified": 0,
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
"type": "path"
},
"original": {
"id": "nixpkgs",
@@ -177,11 +175,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1729256560,
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
@@ -204,11 +202,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1729255720,
"narHash": "sha256-yODOuZxBkS0UfqMa6nmbqNbVfIbsu0tYLbV5vZzmsqI=",
"lastModified": 1731342671,
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "72b214fbfbe6f7b95a7877b962783bd42062cc0a",
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
"type": "github"
},
"original": {

420
flake.nix
View File

@@ -8,8 +8,18 @@
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
android.url = "github:tadfisher/android-nixpkgs";
};
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
flake-utils.lib.eachDefaultSystem (system:
outputs =
{ self
, nixpkgs
, flake-utils
, nix-filter
, naersk
, fenix
, android
,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs.stdenv) isDarwin;
@@ -18,9 +28,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 = ./.;
@@ -173,15 +183,17 @@
# Use DWARF-2 instead of SJLJ for exception handling.
winCC = pkgsWin32.buildPackages.wrapCC (
(pkgsWin32.buildPackages.gcc-unwrapped.override
({
{
threadsCross = {
model = "win32";
package = null;
};
})).overrideAttrs (oldAttr: {
configureFlags = oldAttr.configureFlags ++ [
"--disable-sjlj-exceptions --with-dwarf2"
];
}).overrideAttrs (oldAttr: {
configureFlags =
oldAttr.configureFlags
++ [
"--disable-sjlj-exceptions --with-dwarf2"
];
})
);
in
@@ -232,7 +244,7 @@
rustc = toolchain;
};
in
naersk-lib.buildPackage rec {
naersk-lib.buildPackage {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
@@ -243,27 +255,25 @@
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
};
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:
@@ -317,33 +327,32 @@
{
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
"deltachat-rpc-server-${arch}" = rpc-server;
"deltachat-rpc-server-${arch}-wheel" =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
"deltachat-rpc-server-${arch}-wheel" = pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
};
in
{
@@ -355,6 +364,8 @@
mkRustPackages "x86_64-linux" //
mkRustPackages "armv7l-linux" //
mkRustPackages "armv6l-linux" //
mkRustPackages "x86_64-darwin" //
mkRustPackages "aarch64-darwin" //
mkAndroidPackages "armeabi-v7a" //
mkAndroidPackages "arm64-v8a" //
mkAndroidPackages "x86" //
@@ -365,188 +376,181 @@
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win64-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win64-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
deltachat-rpc-server-win64-wheel = pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win64-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
buildInputs = [
deltachat-rpc-server-win64
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win64
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win32-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win32-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
deltachat-rpc-server-win32-wheel = pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win32-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
buildInputs = [
deltachat-rpc-server-win32
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win32
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
# Run `nix build .#docs` to get C docs generated in `./result/`.
docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
docs = pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
libdeltachat =
pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
libdeltachat = pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
# Source package for deltachat-rpc-server.
# Fake package that downloads Linux version,
# needed to install deltachat-rpc-server on Android with `pip`.
deltachat-rpc-server-source =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-source";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-server-source = pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-source";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-client =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
};
deltachat-rpc-client = pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
};
deltachat-python =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-python";
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
libdeltachat
];
nativeBuildInputs = [
pkgs.pkg-config
];
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.pkgconfig
pkgs.python3Packages.cffi
pkgs.python3Packages.imap-tools
pkgs.python3Packages.pluggy
pkgs.python3Packages.requests
];
};
python-docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
deltachat-python = pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-python";
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
libdeltachat
];
nativeBuildInputs = [
pkgs.pkg-config
];
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.pkgconfig
pkgs.python3Packages.cffi
pkgs.python3Packages.imap-tools
pkgs.python3Packages.pluggy
pkgs.python3Packages.requests
];
};
python-docs = pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
};
devShells.default = let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
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
];
};
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
];
};
}
);
}

View File

@@ -50,6 +50,7 @@ module.exports = {
DC_EVENT_IMEX_PROGRESS: 2051,
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
DC_EVENT_INCOMING_REACTION: 2002,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,

View File

@@ -16,6 +16,7 @@ module.exports = {
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',

View File

@@ -50,6 +50,7 @@ export enum C {
DC_EVENT_IMEX_PROGRESS = 2051,
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
DC_EVENT_INCOMING_REACTION = 2002,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
@@ -322,6 +323,7 @@ export const EventId2EventName: { [key: number]: string } = {
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.147.1"
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

@@ -308,7 +308,7 @@ class Chat:
msg = as_dc_charpointer(text)
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
if msg_id == 0:
raise ValueError("message could not be send, does chat exist?")
raise ValueError("The message could not be sent. Does the chat exist?")
return Message.from_db(self.account, msg_id)
def send_file(self, path, mime_type="application/octet-stream"):

View File

@@ -2209,6 +2209,19 @@ def test_configure_error_msgs_wrong_pw(acfactory):
# Password is wrong so it definitely has to say something about "password"
assert "password" in ev.data2
ac1.stop_io()
ac1.set_config("mail_pw", "abc") # Wrong mail pw
ac1.configure()
while True:
ev = ac1._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
print(f"Configuration progress: {ev.data1}")
if ev.data1 == 0:
break
assert "password" in ev.data2
# Account will continue to work with the old password, so if it becomes wrong, a notification
# must be shown.
assert ac1.get_config("notify_about_wrong_pw") == "1"
def test_configure_error_msgs_invalid_server(acfactory):
ac2 = acfactory.get_unconfigured_account()

View File

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

View File

@@ -5,7 +5,8 @@ use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
use futures::future::join_all;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
@@ -301,20 +302,48 @@ impl Accounts {
///
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_without_timeout(&self) {
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
async fn background_fetch_and_log_error(account: Context) {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
}
join_all(
self.accounts
.values()
.cloned()
.map(background_fetch_and_log_error),
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Starting background fetch for {} accounts.",
accounts.len()
)),
});
let mut futures_unordered: FuturesUnordered<_> = accounts
.into_iter()
.map(background_fetch_and_log_error)
.collect();
while futures_unordered.next().await.is_some() {}
}
/// Auxiliary function for [Accounts::background_fetch].
async fn background_fetch_with_timeout(
accounts: Vec<Context>,
events: Events,
timeout: std::time::Duration,
) {
if let Err(_err) = tokio::time::timeout(
timeout,
Self::background_fetch_no_timeout(accounts, events.clone()),
)
.await;
.await
{
events.emit(Event {
id: 0,
typ: EventType::Warning("Background fetch timed out.".to_string()),
});
}
events.emit(Event {
id: 0,
typ: EventType::AccountsBackgroundFetchDone,
});
}
/// Performs a background fetch for all accounts in parallel with a timeout.
@@ -322,15 +351,13 @@ impl Accounts {
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
/// process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
pub async fn background_fetch(&self, timeout: std::time::Duration) {
if let Err(_err) =
tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await
{
self.emit_event(EventType::Warning(
"Background fetch timed out.".to_string(),
));
}
self.emit_event(EventType::AccountsBackgroundFetchDone);
///
/// Returns a future that resolves when background fetch is done,
/// but does not capture `&self`.
pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
let events = self.events.clone();
Self::background_fetch_with_timeout(accounts, events, timeout)
}
/// Emits a single event.
@@ -344,7 +371,7 @@ impl Accounts {
}
/// Sets notification token for Apple Push Notification service.
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
self.push_subscriber.set_device_token(token).await;
Ok(())
}

View File

@@ -520,8 +520,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 +538,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

@@ -797,8 +797,7 @@ impl ChatId {
context.scheduler.interrupt_inbox().await;
if chat.is_self_talk() {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::self_deleted_msg_body(context).await;
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
}
chatlist_events::emit_chatlist_changed(context);
@@ -2092,28 +2091,31 @@ impl Chat {
EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
};
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
let new_mime_headers = if msg.has_html() {
let html = if msg.param.exists(Param::Forwarded) {
if msg.param.exists(Param::Forwarded) {
msg.get_id().get_html(context).await?
} else {
msg.param.get(Param::SendHtml).map(|s| s.to_string())
};
match html {
Some(html) => Some(tokio::task::block_in_place(move || {
buf_compress(new_html_mimepart(html).build().as_string().as_bytes())
})?),
None => None,
}
} else {
None
};
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
true => Some(msg.text.clone()),
false => None,
});
let new_mime_headers = match new_mime_headers {
Some(h) => Some(tokio::task::block_in_place(move || {
buf_compress(new_html_mimepart(h).build().as_string().as_bytes())
})?),
None => None,
};
msg.chat_id = self.id;
msg.from_id = ContactId::SELF;
msg.rfc724_mid = new_rfc724_mid;
msg.timestamp_sort = timestamp;
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
let mime_modified = new_mime_headers.is_some() | was_truncated;
// add message to the database
if let Some(update_msg_id) = update_msg_id {
@@ -2142,7 +2144,7 @@ impl Chat {
msg.hidden,
msg.in_reply_to.as_deref().unwrap_or_default(),
new_references,
mime_modified,
new_mime_headers.is_some(),
new_mime_headers.unwrap_or_default(),
location_id as i32,
ephemeral_timer,
@@ -2193,7 +2195,7 @@ impl Chat {
msg.hidden,
msg.in_reply_to.as_deref().unwrap_or_default(),
new_references,
mime_modified,
new_mime_headers.is_some(),
new_mime_headers.unwrap_or_default(),
location_id as i32,
ephemeral_timer,
@@ -3103,8 +3105,7 @@ pub async fn send_text_msg(
chat_id
);
let mut msg = Message::new(Viewtype::Text);
msg.text = text_to_send;
let mut msg = Message::new_text(text_to_send);
send_msg(context, chat_id, &mut msg).await
}
@@ -4504,6 +4505,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,
@@ -4777,8 +4779,7 @@ mod tests {
async fn test_get_draft() {
let t = TestContext::new().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
let mut msg = Message::new_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
let draft = chat_id.get_draft(&t).await.unwrap().unwrap();
@@ -4792,13 +4793,11 @@ mod tests {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi!".to_string());
let mut msg = Message::new_text("hi!".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert!(chat_id.get_draft(&t).await?.is_some());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("another".to_string());
let mut msg = Message::new_text("another".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert!(chat_id.get_draft(&t).await?.is_some());
@@ -4812,8 +4811,7 @@ mod tests {
async fn test_forwarding_draft_failing() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
let mut msg = Message::new_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
@@ -4826,8 +4824,7 @@ mod tests {
async fn test_draft_stable_ids() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
let mut msg = Message::new_text("hello".to_string());
assert_eq!(msg.id, MsgId::new_unset());
assert!(chat_id.get_draft_msg_id(&t).await?.is_none());
@@ -4873,11 +4870,7 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let msgs: Vec<message::Message> = (1..=1000)
.map(|i| {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(i.to_string());
msg
})
.map(|i| Message::new_text(i.to_string()))
.collect();
let mut tasks = Vec::new();
for mut msg in msgs {
@@ -4910,8 +4903,7 @@ mod tests {
.await?;
// save a draft
let mut draft = Message::new(Viewtype::Text);
draft.set_text("draft text".to_string());
let mut draft = Message::new_text("draft text".to_string());
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
@@ -4964,29 +4956,25 @@ mod tests {
let one2one_msg = Message::load_from_db(&alice, one2one_msg_id).await?;
// quoting messages in same chat is okay
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
let mut msg = Message::new_text("baz".to_string());
msg.set_quote(&alice, Some(&grp_msg)).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_ok());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
let mut msg = Message::new_text("baz".to_string());
msg.set_quote(&alice, Some(&one2one_msg)).await?;
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
assert!(result.is_ok());
let one2one_quote_reply_msg_id = result.unwrap();
// quoting messages from groups to one-to-ones is okay ("reply privately")
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
let mut msg = Message::new_text("baz".to_string());
msg.set_quote(&alice, Some(&grp_msg)).await?;
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
assert!(result.is_ok());
// quoting messages from one-to-one chats in groups is an error; usually this is also not allowed by UI at all ...
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
let mut msg = Message::new_text("baz".to_string());
msg.set_quote(&alice, Some(&one2one_msg)).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_err());
@@ -5383,7 +5371,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(())
}
@@ -5478,13 +5466,11 @@ mod tests {
let t = TestContext::new().await;
// add two device-messages
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text("first message".to_string());
let mut msg1 = Message::new_text("first message".to_string());
let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await;
assert!(msg1_id.is_ok());
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text("second message".to_string());
let mut msg2 = Message::new_text("second message".to_string());
let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await;
assert!(msg2_id.is_ok());
assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap());
@@ -5513,14 +5499,12 @@ mod tests {
let t = TestContext::new().await;
// add two device-messages with the same label (second attempt is not added)
let mut msg1 = Message::new(Viewtype::Text);
msg1.text = "first message".to_string();
let mut msg1 = Message::new_text("first message".to_string());
let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await;
assert!(msg1_id.is_ok());
assert!(!msg1_id.as_ref().unwrap().is_unset());
let mut msg2 = Message::new(Viewtype::Text);
msg2.text = "second message".to_string();
let mut msg2 = Message::new_text("second message".to_string());
let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await;
assert!(msg2_id.is_ok());
assert!(msg2_id.as_ref().unwrap().is_unset());
@@ -5567,8 +5551,7 @@ mod tests {
let res = add_device_msg(&t, Some("some-label"), None).await;
assert!(res.is_ok());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
let mut msg = Message::new_text("message text".to_string());
let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await;
assert!(msg_id.is_ok());
@@ -5585,8 +5568,7 @@ mod tests {
add_device_msg(&t, Some("some-label"), None).await.ok();
assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
let mut msg = Message::new_text("message text".to_string());
add_device_msg(&t, Some("another-label"), Some(&mut msg))
.await
.ok();
@@ -5603,8 +5585,7 @@ mod tests {
async fn test_delete_device_chat() {
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
let mut msg = Message::new_text("message text".to_string());
add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.ok();
@@ -5627,8 +5608,7 @@ mod tests {
.await
.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
let mut msg = Message::new_text("message text".to_string());
assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err());
assert!(prepare_msg(&t, device_chat_id, &mut msg).await.is_err());
@@ -5639,8 +5619,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_and_reset_all_device_msgs() {
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
let mut msg = Message::new_text("message text".to_string());
let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.unwrap();
@@ -5672,8 +5651,7 @@ mod tests {
async fn test_archive() {
// create two chats
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foo".to_string());
let mut msg = Message::new_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
.await
@@ -5973,8 +5951,7 @@ mod tests {
let t = TestContext::new().await;
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foo".to_string());
let mut msg = Message::new_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
.await
@@ -6051,8 +6028,7 @@ mod tests {
ChatVisibility::Pinned,
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi!".into());
let mut msg = Message::new_text("hi!".into());
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, alice_chat_id);
@@ -6691,8 +6667,7 @@ mod tests {
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Hi Bob".to_owned());
let mut msg = Message::new_text("Hi Bob".to_owned());
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
@@ -6743,8 +6718,7 @@ mod tests {
let received_msg = bob.recv_msg(&sent_msg).await;
// Bob quotes received message and sends a reply to Alice.
let mut reply = Message::new(Viewtype::Text);
reply.set_text("Reply".to_owned());
let mut reply = Message::new_text("Reply".to_owned());
reply.set_quote(&bob, Some(&received_msg)).await?;
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
let received_reply = alice.recv_msg(&sent_reply).await;
@@ -6827,8 +6801,7 @@ mod tests {
let group_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
add_contact_to_chat(&alice, group_id, bob_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("bla foo".to_owned());
let mut msg = Message::new_text("bla foo".to_owned());
let sent_msg = alice.send_msg(group_id, &mut msg).await;
assert!(sent_msg.payload().contains("secretgrpname"));
assert!(sent_msg.payload().contains("secretname"));

View File

@@ -476,7 +476,6 @@ mod tests {
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
};
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -510,8 +509,7 @@ mod tests {
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
// 2s here.
for chat_id in &[chat_id1, chat_id3, chat_id2] {
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
let mut msg = Message::new_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
}
@@ -755,8 +753,7 @@ mod tests {
.await
.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foo:\nbar \r\n test".to_string());
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();

View File

@@ -321,6 +321,10 @@ pub enum Config {
/// True if account is muted.
IsMuted,
/// Optional tag as "Work", "Family".
/// Meant to help profile owner to differ between profiles with similar names.
PrivateTag,
/// All secondary self addresses separated by spaces
/// (`addr1@example.org addr2@example.org addr3@example.org`)
SecondaryAddrs,
@@ -392,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"))]
@@ -429,6 +439,7 @@ pub enum Config {
WebxdcIntegration,
/// Enable webxdc realtime features.
#[strum(props(default = "1"))]
WebxdcRealtimeEnabled,
}

View File

@@ -19,11 +19,12 @@ use auto_outlook::outlk_autodiscover;
use deltachat_contact_tools::EmailAddress;
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use percent_encoding::utf8_percent_encode;
use server_params::{expand_param_vector, ServerParams};
use tokio::task;
use crate::config::{self, Config};
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::LogExt;
@@ -31,7 +32,7 @@ use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
};
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
@@ -111,15 +112,10 @@ impl Context {
let param = EnteredLoginParam::load(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let configured_param_res = configure(self, &param).await;
self.set_config_internal(Config::NotifyAboutWrongPw, None)
.await?;
on_configure_completed(self, configured_param_res?, old_addr).await?;
let configured_param = configure(self, &param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, configured_param, old_addr).await?;
Ok(())
}
}
@@ -147,8 +143,7 @@ async fn on_configure_completed(
}
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = provider.after_login_hint.to_string();
let mut msg = Message::new_text(provider.after_login_hint.to_string());
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
@@ -161,9 +156,9 @@ async fn on_configure_completed(
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Some(old_addr) = old_addr {
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new(Viewtype::Text);
msg.text =
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await;
let mut msg = Message::new_text(
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
@@ -417,7 +412,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
configured_param.oauth2,
r,
);
let mut imap_session = match imap.connect(ctx).await {
let configuring = true;
let mut imap_session = match imap.connect(ctx, configuring).await {
Ok(session) => session,
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
};
@@ -503,7 +499,15 @@ async fn get_autoconfig(
param: &EnteredLoginParam,
param_domain: &str,
) -> Option<Vec<ServerParams>> {
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
// Make sure to not encode `.` as `%2E` here.
// Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML
// when address is encoded.
// E.g.
// <https://autoconfig.murena.io/mail/config-v1.1.xml?emailaddress=foobar%40example%2Eorg>
// produced XML file with `<username>foobar@example%2Eorg</username>`
// resulting in failure to log in.
let param_addr_urlencoded =
utf8_percent_encode(&param.addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
if let Ok(res) = moz_autoconfigure(
ctx,

View File

@@ -4,12 +4,16 @@
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
/// Set of characters to percent-encode in email addresses and names.
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
#[derive(
Debug,
Default,

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 {
@@ -425,9 +462,12 @@ pub enum Origin {
/// To: of incoming messages of unknown sender
IncomingUnknownTo = 0x40,
/// address scanned but not verified
/// Address scanned but not verified.
UnhandledQrScan = 0x80,
/// Address scanned from a SecureJoin QR code, but not verified yet.
UnhandledSecurejoinQrScan = 0x81,
/// Reply-To: of incoming message of known sender
/// Contacts with at least this origin value are shown in the contact list.
IncomingReplyTo = 0x100,
@@ -3149,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

@@ -28,7 +28,7 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::peerstate::Peerstate;
@@ -857,6 +857,12 @@ impl Context {
"is_muted",
self.get_config_bool(Config::IsMuted).await?.to_string(),
);
res.insert(
"private_tag",
self.get_config(Config::PrivateTag)
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
if let Some(metadata) = &*self.metadata.read().await {
if let Some(comment) = &metadata.comment {
@@ -984,6 +990,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(),
@@ -1172,8 +1184,7 @@ impl Context {
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = self.get_self_report().await?;
let mut msg = Message::new_text(self.get_self_report().await?);
chat_id.set_draft(self, Some(&mut msg)).await?;
@@ -1772,12 +1783,10 @@ mod tests {
assert!(res.is_empty());
// Add messages to chat with Bob.
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text("foobar".to_string());
let mut msg1 = Message::new_text("foobar".to_string());
send_msg(&alice, chat.id, &mut msg1).await?;
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text("barbaz".to_string());
let mut msg2 = Message::new_text("barbaz".to_string());
send_msg(&alice, chat.id, &mut msg2).await?;
alice.send_text(chat.id, "Δ-Chat").await;
@@ -1880,8 +1889,7 @@ mod tests {
.await;
// Add 999 messages
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foobar".to_string());
let mut msg = Message::new_text("foobar".to_string());
for _ in 0..999 {
send_msg(&alice, chat.id, &mut msg).await?;
}

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
@@ -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

@@ -318,8 +318,7 @@ mod tests {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Hi Bob".to_owned());
let mut msg = Message::new_text("Hi Bob".to_owned());
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);

View File

@@ -223,8 +223,9 @@ impl ChatId {
self.inner_set_ephemeral_timer(context, timer).await?;
if self.is_promoted(context).await? {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await;
let mut msg = Message::new_text(
stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await,
);
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
@@ -1362,8 +1363,7 @@ mod tests {
chat.id
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi".to_string());
let mut msg = Message::new_text("hi".to_string());
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
.await
.is_err());
@@ -1393,8 +1393,7 @@ mod tests {
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
let mut poi_msg = Message::new(Viewtype::Text);
poi_msg.text = "Here".to_string();
let mut poi_msg = Message::new_text("Here".to_string());
poi_msg.set_location(10.0, 20.0);
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;

View File

@@ -8,6 +8,7 @@ use crate::config::Config;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::reaction::Reaction;
use crate::webxdc::StatusUpdateSerial;
/// Event payload.
@@ -94,6 +95,18 @@ pub enum EventType {
contact_id: ContactId,
},
/// Reactions for the message changed.
IncomingReaction {
/// ID of the contact whose reaction set is changed.
contact_id: ContactId,
/// ID of the message for which reactions were changed.
msg_id: MsgId,
/// The reaction.
reaction: Reaction,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///

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>
"#
);
@@ -525,8 +521,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// alice sends a message with html-part to bob
let chat_id = alice.create_chat(&bob).await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("plain text".to_string());
let mut msg = Message::new_text("plain text".to_string());
msg.set_html(Some("<b>html</b> text".to_string()));
assert!(msg.mime_modified);
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();

View File

@@ -36,7 +36,7 @@ use crate::log::LogExt;
use crate::login_param::{
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
use crate::mimeparser;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
@@ -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,
@@ -290,7 +290,11 @@ impl Imap {
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection
/// parameters.
pub(crate) async fn connect(&mut self, context: &Context) -> Result<Session> {
pub(crate) async fn connect(
&mut self,
context: &Context,
configuring: bool,
) -> Result<Session> {
let now = tools::Time::now();
let until_can_send = max(
min(self.conn_last_try, now)
@@ -398,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
@@ -412,37 +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:#})"));
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& err_str.to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context
.set_config_internal(Config::NotifyAboutWrongPw, None)
// If it looks like the password is wrong, send a notification:
let _lock = context.wrong_pw_warning_mutex.lock().await;
if err.to_string().to_lowercase().contains("authentication") {
if self.authentication_failed_once
&& !configuring
&& 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
{
warn!(context, "{e:#}.");
}
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text.clone_from(&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:#}.");
{
warn!(context, "Failed to add device message: {e:#}.");
} else {
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
.log_err(context)
.ok();
}
} else {
self.authentication_failed_once = true;
}
} else {
self.login_failed_once = true;
self.authentication_failed_once = false;
}
}
}
@@ -456,7 +461,8 @@ impl Imap {
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined.
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
let mut session = match self.connect(context).await {
let configuring = false;
let mut session = match self.connect(context, configuring).await {
Ok(session) => session,
Err(err) => {
self.connectivity.set_err(context, &err).await;
@@ -1196,6 +1202,8 @@ impl Session {
.await
.context("failed to fetch flags")?;
let mut got_unsolicited_fetch = false;
while let Some(fetch) = list
.try_next()
.await
@@ -1205,6 +1213,7 @@ impl Session {
uid
} else {
info!(context, "FETCH result contains no UID, skipping");
got_unsolicited_fetch = true;
continue;
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
@@ -1227,6 +1236,15 @@ impl Session {
warn!(context, "FETCH result contains no MODSEQ");
}
}
drop(list);
if got_unsolicited_fetch {
// We got unsolicited FETCH, which means some flags
// have been modified while our request was in progress.
// We may or may not have these new flags as a part of the response,
// so better skip next IDLE and do another round of flag synchronization.
self.new_mail = true;
}
set_modseq(context, folder, highest_modseq)
.await
@@ -1712,17 +1730,21 @@ impl Imap {
}
impl Session {
/// Return whether the server sent an unsolicited EXISTS response.
/// Return whether the server sent an unsolicited EXISTS or FETCH response.
///
/// Drains all responses from `session.unsolicited_responses` in the process.
/// If this returns `true`, this means that new emails arrived and you should
/// fetch again, even if you just fetched.
fn server_sent_unsolicited_exists(&self, context: &Context) -> Result<bool> {
///
/// If this returns `true`, this means that new emails arrived
/// or flags have been changed.
/// In this case we may want to skip next IDLE and do a round
/// of fetching new messages and synchronizing seen flags.
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
use async_imap::imap_proto::Response;
use async_imap::imap_proto::ResponseCode;
use UnsolicitedResponse::*;
let folder = self.selected_folder.as_deref().unwrap_or_default();
let mut unsolicited_exists = false;
let mut should_refetch = false;
while let Ok(response) = self.unsolicited_responses.try_recv() {
match response {
Exists(_) => {
@@ -1730,28 +1752,38 @@ impl Session {
context,
"Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
);
unsolicited_exists = true;
should_refetch = true;
}
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them
Expunge(_) | Recent(_) => {}
Other(response_data)
if matches!(
response_data.parsed(),
Response::Fetch { .. }
| Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
}
) => {}
Other(ref response_data) => {
match response_data.parsed() {
Response::Fetch { .. } => {
info!(
context,
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
);
should_refetch = true;
}
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them.
Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
} => {}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
}
}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
}
}
Ok(unsolicited_exists)
Ok(should_refetch)
}
}
@@ -1884,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

@@ -180,6 +180,7 @@ impl Client {
Client::connect_insecure_proxy(context, host, port, proxy_config).await?
}
};
update_connection_history(context, "imap", host, port, host, time()).await?;
Ok(client)
} else {
let load_cache = match security {

View File

@@ -9,7 +9,6 @@ use tokio::time::timeout;
use super::session::Session;
use super::Imap;
use crate::context::Context;
use crate::imap::FolderMeaning;
use crate::net::TIMEOUT;
use crate::tools::{self, time_elapsed};
@@ -32,7 +31,7 @@ impl Session {
self.select_with_uidvalidity(context, folder).await?;
if self.server_sent_unsolicited_exists(context)? {
if self.drain_unsolicited_responses(context)? {
self.new_mail = true;
}
@@ -109,37 +108,16 @@ impl Imap {
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
session: &mut Session,
watch_folder: String,
folder_meaning: FolderMeaning,
) -> Result<()> {
let fake_idle_start_time = tools::Time::now();
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// Loop until we are interrupted or until we fetch something.
loop {
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
Err(_) => {
// Let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
let res = self
.fetch_new_messages(context, session, &watch_folder, folder_meaning, false)
.await?;
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break;
}
}
Ok(_) => {
info!(context, "Fake IDLE interrupted.");
break;
}
}
// Wait for 60 seconds or until we are interrupted.
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
Err(_) => info!(context, "Fake IDLE finished."),
Ok(_) => info!(context, "Fake IDLE interrupted."),
}
info!(

View File

@@ -66,21 +66,11 @@ impl Imap {
&& folder_meaning != FolderMeaning::Drafts
&& folder_meaning != FolderMeaning::Trash
{
// Drain leftover unsolicited EXISTS messages
session.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !session.server_sent_unsolicited_exists(context)? {
break;
}
}
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
}
}

View File

@@ -42,7 +42,7 @@ use tokio_util::sync::CancellationToken;
use crate::chat::add_device_msg;
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{create_id, time, TempPathGuard};
@@ -200,8 +200,7 @@ impl BackupProvider {
info!(context, "Received backup reception acknowledgement.");
context.emit_event(EventType::ImexProgress(1000));
let mut msg = Message::new(Viewtype::Text);
msg.text = backup_transfer_msg_body(&context).await;
let mut msg = Message::new_text(backup_transfer_msg_body(&context).await);
add_device_msg(&context, None, Some(&mut msg)).await?;
Ok(())
@@ -369,6 +368,7 @@ mod tests {
use std::time::Duration;
use crate::chat::{get_chat_msgs, send_msg, ChatItem};
use crate::message::Viewtype;
use crate::test_utils::TestContextManager;
use super::*;
@@ -382,8 +382,7 @@ mod tests {
// Write a message in the self chat
let self_chat = ctx0.get_self_chat().await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi there".to_string());
let mut msg = Message::new_text("hi there".to_string());
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
// Send an attachment in the self chat

View File

@@ -290,8 +290,7 @@ pub async fn send_locations_to_chat(
)
.await?;
if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::msg_location_enabled(context).await;
let mut msg = Message::new_text(stock_str::msg_location_enabled(context).await);
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
chat::send_msg(context, chat_id, &mut msg)
.await

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(())
}
@@ -491,6 +492,15 @@ impl Message {
}
}
/// Creates a new message with Viewtype::Text.
pub fn new_text(text: String) -> Self {
Message {
viewtype: Viewtype::Text,
text,
..Default::default()
}
}
/// Loads message with given ID from the database.
///
/// Returns an error if the message does not exist.
@@ -1841,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(())
}
@@ -2333,8 +2344,7 @@ mod tests {
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Quoted message".to_string());
let mut msg = Message::new_text("Quoted message".to_string());
// Prepare message for sending, so it gets a Message-Id.
assert!(msg.rfc724_mid.is_empty());
@@ -2400,9 +2410,8 @@ mod tests {
add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?;
// Alice quotes encrypted message in unencrypted chat.
let mut msg = Message::new(Viewtype::Text);
let mut msg = Message::new_text("unencrypted".to_string());
msg.set_quote(alice, Some(&alice_received_message)).await?;
msg.set_text("unencrypted".to_string());
chat::send_msg(alice, alice_group, &mut msg).await?;
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
@@ -2460,8 +2469,7 @@ mod tests {
.unwrap();
let contact = Contact::get_by_id(&alice, contact_id).await.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text("bla blubb".to_string());
let mut msg = Message::new_text("bla blubb".to_string());
msg.set_override_sender_name(Some("over ride".to_string()));
assert_eq!(
msg.get_override_sender_name(),
@@ -2508,8 +2516,7 @@ mod tests {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("this is the text!".to_string());
let mut msg = Message::new_text("this is the text!".to_string());
// alice sends to bob,
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
@@ -2594,8 +2601,7 @@ mod tests {
}
// check outgoing messages states on sender side
let mut alice_msg = Message::new(Viewtype::Text);
alice_msg.set_text("hi!".to_string());
let mut alice_msg = Message::new_text("hi!".to_string());
assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work
alice_chat
@@ -2778,8 +2784,7 @@ def hello():
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi".to_string());
let mut msg = Message::new_text("hi".to_string());
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
.await
.is_err());

View File

@@ -20,6 +20,7 @@ use crate::e2ee::EncryptHelper;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::headerdef::HeaderDef;
use crate::html::new_html_mimepart;
use crate::location;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
@@ -32,7 +33,6 @@ use crate::tools::{
create_outgoing_rfc724_mid, create_smeared_timestamp, remove_subject_prefix, time,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{location, peer_channels};
// attachments of 25 mb brutto should work on the majority of providers
// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
@@ -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:
@@ -1387,8 +1389,7 @@ impl MimeFactory {
let json = msg.param.get(Param::Arg).unwrap_or_default();
parts.push(context.build_status_update_part(json));
} else if msg.viewtype == Viewtype::Webxdc {
let topic = peer_channels::create_random_topic();
headers.push(create_iroh_header(context, topic, msg.id).await?);
headers.push(create_iroh_header(context, msg.id).await?);
if let (Some(json), _) = context
.render_webxdc_status_update_object(
msg.id,
@@ -1983,8 +1984,7 @@ mod tests {
group_id: ChatId,
quote: Option<&Message>,
) -> Result<String> {
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text("Hi".to_string());
let mut new_msg = Message::new_text("Hi".to_string());
if let Some(q) = quote {
new_msg.set_quote(t, Some(q)).await?;
}
@@ -2070,8 +2070,7 @@ mod tests {
let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text("Hi".to_string());
let mut new_msg = Message::new_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
@@ -2178,8 +2177,7 @@ mod tests {
let chat_id = chats.get_chat_id(0).unwrap();
chat_id.accept(context).await.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text("Hi".to_string());
let mut new_msg = Message::new_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::prepare_msg(context, chat_id, &mut new_msg)
.await
@@ -2296,8 +2294,7 @@ mod tests {
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text("this is the text!".to_string());
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
@@ -2363,8 +2360,7 @@ mod tests {
// send message to bob: that should get multipart/signed.
// `Subject:` is protected by copying it.
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text("this is the text!".to_string());
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
@@ -2497,8 +2493,7 @@ mod tests {
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text("this is the text!".to_string());
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let payload = sent_msg.payload();

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
@@ -1158,7 +1221,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
};
@@ -1168,7 +1231,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
};
@@ -1231,7 +1294,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"
@@ -2240,12 +2303,22 @@ async fn handle_ndn(
} else {
"Delivery to at least one recipient failed.".to_string()
};
let err_msg = &error;
let mut first = true;
for msg in msgs {
let (msg_id, chat_id, chat_type) = msg?;
let mut message = Message::load_from_db(context, msg_id).await?;
set_msg_failed(context, &mut message, &error).await?;
let aggregated_error = message
.error
.as_ref()
.map(|err| format!("{}\n\n{}", err, err_msg));
set_msg_failed(
context,
&mut message,
aggregated_error.as_ref().unwrap_or(err_msg),
)
.await?;
if first {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
@@ -3598,11 +3671,24 @@ On 2020-10-25, Bob wrote:
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
}
{
for draft in [false, true] {
let chat = t.get_self_chat().await;
t.send_text(chat.id, &long_txt).await;
let mut msg = Message::new_text(long_txt.clone());
if draft {
chat.id.set_draft(&t, Some(&mut msg)).await?;
}
t.send_msg(chat.id, &mut msg).await;
let msg = t.get_last_msg_in(chat.id).await;
assert!(msg.has_html());
assert_eq!(
msg.id
.get_html(&t)
.await?
.unwrap()
.matches("just repeated")
.count(),
REPEAT_CNT
);
assert!(
msg.text.matches("just repeated").count() <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
@@ -3610,7 +3696,6 @@ On 2020-10-25, Bob wrote:
}
t.set_config(Config::Bot, Some("1")).await?;
{
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?;
assert!(!mimemsg.is_mime_modified);
@@ -3623,6 +3708,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;
@@ -3990,12 +4097,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(())
@@ -4066,4 +4169,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;
@@ -47,6 +47,14 @@ pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
Ok(())
}
/// Update the timestamp of the last successfull connection
/// to the given `host` and `port`
/// with the given application protocol `alpn`.
///
/// `addr` is the string representation of IP address.
/// If connection is made over a proxy which does
/// its own DNS resolution,
/// `addr` should be the same as `host`.
pub(crate) async fn update_connection_history(
context: &Context,
alpn: &str,
@@ -120,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

@@ -1,4 +1,44 @@
//! DNS resolution and cache.
//!
//! DNS cache in Delta Chat has two layers:
//! in-memory cache and persistent `dns_cache` SQL table.
//!
//! In-memory cache is using a "stale-while-revalidate" strategy.
//! If there is a cached value, it is returned immediately
//! and revalidation task is started in the background
//! to replace old cached IP addresses with new ones.
//! If there is no cached value yet,
//! lookup only finishes when `lookup_host` returns first results.
//! In-memory cache is shared between all accounts
//! and is never stored on the disk.
//! It can be thought of as an extension
//! of the system resolver.
//!
//! Persistent `dns_cache` SQL table is used to collect
//! all IP addresses ever seen for the hostname
//! together with the timestamp
//! of the last time IP address has been seen.
//! Note that this timestamp reflects the time
//! IP address was returned by the in-memory cache
//! rather than the underlying system resolver.
//! Unused entries are removed after 30 days
//! (`CACHE_TTL` constant) to avoid having
//! old non-working IP addresses in the cache indefinitely.
//!
//! When Delta Chat needs an IP address for the host,
//! it queries in-memory cache for the next result
//! and merges the list of IP addresses
//! with the list of IP addresses from persistent cache.
//! Resulting list is constructed
//! by taking the first two results from the resolver
//! followed up by persistent cache results
//! and terminated by the rest of resolver results.
//!
//! Persistent cache results are sorted
//! by the time of the most recent successful connection
//! using the result. For results that have never been
//! used for successful connection timestamp of
//! retrieving them from in-memory cache is used.
use anyhow::{Context as _, Result};
use std::collections::HashMap;
@@ -42,33 +82,110 @@ pub(crate) async fn prune_dns_cache(context: &Context) -> Result<()> {
Ok(())
}
/// Looks up the hostname and updates DNS cache
/// on success.
/// Map from hostname to IP addresses.
///
/// NOTE: sync RwLock is used, so it must not be held across `.await`
/// to avoid deadlocks.
/// See
/// <https://docs.rs/tokio/1.40.0/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use>
/// and
/// <https://stackoverflow.com/questions/63712823/why-do-i-get-a-deadlock-when-using-tokio-with-a-stdsyncmutex>.
static LOOKUP_HOST_CACHE: Lazy<parking_lot::RwLock<HashMap<String, Vec<IpAddr>>>> =
Lazy::new(Default::default);
/// Wrapper for `lookup_host` that returns IP addresses.
async fn lookup_ips(host: impl tokio::net::ToSocketAddrs) -> Result<impl Iterator<Item = IpAddr>> {
Ok(lookup_host(host)
.await
.context("DNS lookup failure")?
.map(|addr| addr.ip()))
}
async fn lookup_host_with_memory_cache(
context: &Context,
hostname: &str,
port: u16,
) -> Result<Vec<IpAddr>> {
let stale_result = {
let rwlock_read_guard = LOOKUP_HOST_CACHE.read();
rwlock_read_guard.get(hostname).cloned()
};
if let Some(stale_result) = stale_result {
// Revalidate the cache in the background.
{
let context = context.clone();
let hostname = hostname.to_string();
tokio::spawn(async move {
match lookup_ips((hostname.clone(), port)).await {
Ok(res) => {
LOOKUP_HOST_CACHE.write().insert(hostname, res.collect());
}
Err(err) => {
warn!(
context,
"Failed to revalidate results for {hostname:?}: {err:#}."
);
}
}
});
}
info!(
context,
"Using memory-cached DNS resolution for {hostname}."
);
Ok(stale_result)
} else {
info!(
context,
"No memory-cached DNS resolution for {hostname} available, waiting for the resolver."
);
let res: Vec<IpAddr> = lookup_ips((hostname, port)).await?.collect();
// Insert initial result into the cache.
//
// There may already be a result from a parallel
// task stored, overwriting it is not a problem.
LOOKUP_HOST_CACHE
.write()
.insert(hostname.to_string(), res.clone());
Ok(res)
}
}
/// Looks up the hostname and updates
/// persistent DNS cache on success.
async fn lookup_host_and_update_cache(
context: &Context,
hostname: &str,
port: u16,
now: i64,
) -> Result<Vec<SocketAddr>> {
let res: Vec<SocketAddr> = timeout(super::TIMEOUT, lookup_host((hostname, port)))
.await
.context("DNS lookup timeout")?
.context("DNS lookup failure")?
.collect();
let res: Vec<IpAddr> = timeout(
super::TIMEOUT,
lookup_host_with_memory_cache(context, hostname, port),
)
.await
.context("DNS lookup timeout")?
.context("DNS lookup with memory cache failure")?;
for addr in &res {
let ip_string = addr.ip().to_string();
for ip in &res {
let ip_string = ip.to_string();
if ip_string == hostname {
// IP address resolved into itself, not interesting to cache.
continue;
}
info!(context, "Resolved {hostname}:{port} into {addr}.");
info!(context, "Resolved {hostname} into {ip}.");
// Update the cache.
update_cache(context, hostname, &ip_string, now).await?;
}
let res = res
.into_iter()
.map(|ip| SocketAddr::new(ip, port))
.collect();
Ok(res)
}

View File

@@ -12,13 +12,14 @@ use fast_socks5::client::Socks5Stream;
use fast_socks5::util::target_addr::ToTargetAddr;
use fast_socks5::AuthenticationMethod;
use fast_socks5::Socks5Command;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use percent_encoding::{percent_encode, utf8_percent_encode, NON_ALPHANUMERIC};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
use url::Url;
use crate::config::Config;
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::net::connect_tcp;
use crate::net::session::SessionStream;
@@ -41,6 +42,12 @@ impl PartialEq for ShadowsocksConfig {
impl Eq for ShadowsocksConfig {}
impl ShadowsocksConfig {
fn to_url(&self) -> String {
self.server_config.to_url()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpConfig {
/// HTTP proxy host.
@@ -84,6 +91,17 @@ impl HttpConfig {
};
Ok(http_config)
}
fn to_url(&self, scheme: &str) -> String {
let host = utf8_percent_encode(&self.host, NON_ALPHANUMERIC_WITHOUT_DOT);
if let Some((user, password)) = &self.user_password {
let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
format!("{scheme}://{user}:{password}@{host}:{}", self.port)
} else {
format!("{scheme}://{host}:{}", self.port)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -123,6 +141,17 @@ impl Socks5Config {
Ok(socks_stream)
}
fn to_url(&self) -> String {
let host = utf8_percent_encode(&self.host, NON_ALPHANUMERIC_WITHOUT_DOT);
if let Some((user, password)) = &self.user_password {
let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
format!("socks5://{user}:{password}@{host}:{}", self.port)
} else {
format!("socks5://{host}:{}", self.port)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -217,7 +246,7 @@ where
impl ProxyConfig {
/// Creates a new proxy configuration by parsing given proxy URL.
fn from_url(url: &str) -> Result<Self> {
pub(crate) fn from_url(url: &str) -> Result<Self> {
let url = Url::parse(url).context("Cannot parse proxy URL")?;
match url.scheme() {
"http" => {
@@ -272,6 +301,19 @@ impl ProxyConfig {
}
}
/// Serializes proxy config into an URL.
///
/// This function can be used to normalize proxy URL
/// by parsing it and serializing back.
pub(crate) fn to_url(&self) -> String {
match self {
Self::Http(http_config) => http_config.to_url("http"),
Self::Https(http_config) => http_config.to_url("https"),
Self::Socks5(socks5_config) => socks5_config.to_url(),
Self::Shadowsocks(shadowsocks_config) => shadowsocks_config.to_url(),
}
}
/// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password`
/// config into `proxy_url` if `proxy_url` is unset or empty.
///

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

@@ -23,7 +23,7 @@
//! (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
//! and `joinRealtimeChannel().send()` just like the other peers.
use anyhow::{anyhow, Context as _, Result};
use anyhow::{anyhow, bail, Context as _, Result};
use email::Header;
use futures_lite::StreamExt;
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
@@ -143,9 +143,10 @@ impl Iroh {
self.endpoint.add_node_addr(peer.clone())?;
}
self.gossip
.join(topic, peers.into_iter().map(|peer| peer.node_id).collect())
.await?;
self.gossip.join_with_opts(
topic,
JoinOptions::with_bootstrap(peers.into_iter().map(|peer| peer.node_id)),
);
}
Ok(())
}
@@ -232,18 +233,24 @@ impl ChannelState {
impl Context {
/// Create iroh endpoint and gossip.
async fn init_peer_channels(&self) -> Result<Iroh> {
info!(self, "Initializing peer channels.");
let secret_key = SecretKey::generate();
let public_key = secret_key.public();
let relay_url = self
let relay_mode = if let Some(relay_url) = self
.metadata
.read()
.await
.as_ref()
.and_then(|conf| conf.iroh_relay.clone())
.unwrap_or(Url::parse("https://iroh.testrun.org:4443").unwrap());
{
RelayMode::Custom(RelayMap::from_url(RelayUrl::from(relay_url)))
} else {
// FIXME: this should be RelayMode::Disabled instead.
// Currently using default relays because otherwise Rust tests fail.
RelayMode::Default
};
let relay_mode = RelayMode::Custom(RelayMap::from_url(RelayUrl::from(relay_url)));
let endpoint = Endpoint::builder()
.secret_key(secret_key)
.alpns(vec![GOSSIP_ALPN.to_vec()])
@@ -253,7 +260,14 @@ impl Context {
// create gossip
let my_addr = endpoint.node_addr().await?;
let gossip = Gossip::from_endpoint(endpoint.clone(), Default::default(), &my_addr.info);
let gossip_config = iroh_gossip::proto::topic::Config {
// Allow messages up to 128 KB in size.
// We set the limit to 128 KiB to account for internal overhead,
// but only guarantee 128 KB of payload to WebXDC developers.
max_message_size: 128 * 1024,
..Default::default()
};
let gossip = Gossip::from_endpoint(endpoint.clone(), gossip_config, &my_addr.info);
// spawn endpoint loop that forwards incoming connections to the gossiper
let context = self.clone();
@@ -272,6 +286,10 @@ impl Context {
/// Get or initialize the iroh peer channel.
pub async fn get_or_try_init_peer_channel(&self) -> Result<&Iroh> {
if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
bail!("Attempt to get Iroh when realtime is disabled");
}
let ctx = self.clone();
self.iroh
.get_or_try_init(|| async { ctx.init_peer_channels().await })
@@ -296,6 +314,47 @@ pub(crate) async fn iroh_add_peer_for_topic(
Ok(())
}
/// Add gossip peer from `Iroh-Node-Addr` header to WebXDC message identified by `instance_id`.
pub async fn add_gossip_peer_from_header(
context: &Context,
instance_id: MsgId,
node_addr: &str,
) -> Result<()> {
if !context
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await?
{
return Ok(());
}
info!(
context,
"Adding iroh peer with address {node_addr:?} to the topic of {instance_id}."
);
let node_addr =
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
msg_id: instance_id,
});
let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? else {
warn!(
context,
"Could not add iroh peer because {instance_id} has no topic."
);
return Ok(());
};
let node_id = node_addr.node_id;
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
let iroh = context.get_or_try_init_peer_channel().await?;
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
Ok(())
}
/// Insert topicId into the database so that we can use it to retrieve the topic.
pub(crate) async fn insert_topic_stub(ctx: &Context, msg_id: MsgId, topic: TopicId) -> Result<()> {
ctx.sql
@@ -409,15 +468,15 @@ pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
Ok(())
}
pub(crate) fn create_random_topic() -> TopicId {
/// Creates a new random gossip topic.
fn create_random_topic() -> TopicId {
TopicId::from_bytes(rand::random())
}
pub(crate) async fn create_iroh_header(
ctx: &Context,
topic: TopicId,
msg_id: MsgId,
) -> Result<Header> {
/// Creates `Iroh-Gossip-Header` with a new random topic
/// and stores the topic for the message.
pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result<Header> {
let topic = create_random_topic();
insert_topic_stub(ctx, msg_id, topic).await?;
Ok(Header::new(
HeaderDef::IrohGossipTopic.get_headername().to_string(),
@@ -531,17 +590,6 @@ mod tests {
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
// Alice sends webxdc to bob
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
@@ -680,17 +728,6 @@ mod tests {
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
assert!(alice
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await
@@ -848,17 +885,6 @@ mod tests {
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
// Alice sends webxdc to bob
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
@@ -927,6 +953,11 @@ mod tests {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
alice
.set_config_bool(Config::WebxdcRealtimeEnabled, false)
.await
.unwrap();
// creates iroh endpoint as side effect
send_webxdc_realtime_advertisement(alice, MsgId::new(1))
.await
@@ -944,6 +975,10 @@ mod tests {
// creates iroh endpoint as side effect
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
assert!(alice.ctx.iroh.get().is_none())
assert!(alice.ctx.iroh.get().is_none());
// This internal function should return error
// if accidentally called with the setting disabled.
assert!(alice.ctx.get_or_try_init_peer_channel().await.is_err());
}
}

View File

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

View File

@@ -297,34 +297,34 @@ 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() {
@@ -333,7 +333,7 @@ pub fn pk_decrypt(
}
}
}
Ok((content, ret_signature_fingerprints))
Ok(ret_signature_fingerprints)
}
/// Validates detached signature.
@@ -407,6 +407,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 +546,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 +583,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

@@ -31,7 +31,7 @@ impl PushSubscriber {
}
/// Sets device token for Apple Push Notification service.
pub(crate) async fn set_device_token(&mut self, token: &str) {
pub(crate) async fn set_device_token(&self, token: &str) {
self.inner.write().await.device_token = Some(token.to_string());
}

View File

@@ -20,7 +20,7 @@ use crate::events::EventType;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::net::http::post_empty;
use crate::net::proxy::DEFAULT_SOCKS_PORT;
use crate::net::proxy::{ProxyConfig, DEFAULT_SOCKS_PORT};
use crate::peerstate::Peerstate;
use crate::token;
use crate::tools::validate_id;
@@ -446,7 +446,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan)
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledSecurejoinQrScan)
.await
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
@@ -723,6 +723,10 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
.get_config(Config::ProxyUrl)
.await?
.unwrap_or_default();
// Normalize the URL.
let url = ProxyConfig::from_url(&url)?.to_url();
let proxy_urls: Vec<&str> = std::iter::once(url.as_str())
.chain(
old_proxy_url_value
@@ -1266,7 +1270,8 @@ mod tests {
if let Qr::AskVerifyContact { contact_id, .. } = qr {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "cli@deltachat.de");
assert_eq!(contact.get_name(), "Jörn P. P.");
assert_eq!(contact.get_authname(), "Jörn P. P.");
assert_eq!(contact.get_name(), "");
} else {
bail!("Wrong QR code type");
}
@@ -1281,6 +1286,7 @@ mod tests {
if let Qr::AskVerifyContact { contact_id, .. } = qr {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "cli@deltachat.de");
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
} else {
bail!("Wrong QR code type");
@@ -1787,6 +1793,17 @@ mod tests {
)
);
// SOCKS5 config does not have port 1080 explicitly specified,
// but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry.
set_config_from_qr(&t, "socks5://1.2.3.4").await?;
assert_eq!(
t.get_config(Config::ProxyUrl).await?,
Some(
"socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080"
.to_string()
)
);
Ok(())
}

View File

@@ -14,6 +14,77 @@ use crate::qr::{self, Qr};
use crate::securejoin;
use crate::stock_str::{self, backup_transfer_qr};
/// Create a QR code from any input data.
pub fn create_qr_svg(qrcode_content: &str) -> Result<String> {
let all_size = 512.0;
let qr_code_size = 416.0;
let logo_size = 96.0;
let qr = QrCode::encode_text(qrcode_content, QrCodeEcc::Medium)?;
let mut svg = String::with_capacity(28000);
let mut w = tagger::new(&mut svg);
w.elem("svg", |d| {
d.attr("xmlns", "http://www.w3.org/2000/svg")?;
d.attr("viewBox", format_args!("0 0 {all_size} {all_size}"))?;
d.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")?; // required for enabling xlink:href on browsers
Ok(())
})?
.build(|w| {
// background
w.single("rect", |d| {
d.attr("x", 0)?;
d.attr("y", 0)?;
d.attr("width", all_size)?;
d.attr("height", all_size)?;
d.attr("style", "fill:#ffffff")?;
Ok(())
})?;
// QR code
w.elem("g", |d| {
d.attr(
"transform",
format!(
"translate({},{})",
(all_size - qr_code_size) / 2.0,
((all_size - qr_code_size) / 2.0)
),
)
})?
.build(|w| {
w.single("path", |d| {
let mut path_data = String::with_capacity(0);
let scale = qr_code_size / qr.size() as f32;
for y in 0..qr.size() {
for x in 0..qr.size() {
if qr.get_module(x, y) {
path_data += &format!("M{x},{y}h1v1h-1z");
}
}
}
d.attr("style", "fill:#000000")?;
d.attr("d", path_data)?;
d.attr("transform", format!("scale({scale})"))
})
})?;
w.elem("g", |d| {
d.attr(
"transform",
format!(
"translate({},{}) scale(2)", // data in qr_overlay_delta.svg-part are 48 x 48, scaling by 2 results in desired logo_size of 96
(all_size - logo_size) / 2.0,
(all_size - logo_size) / 2.0
),
)
})?
.build(|w| w.put_raw_escapable(include_str!("../assets/qr_overlay_delta.svg-part")))
})?;
Ok(svg)
}
/// Returns SVG of the QR code to join the group or verify contact.
///
/// If `chat_id` is `None`, returns verification QR code.
@@ -304,6 +375,14 @@ mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_qr_svg() -> Result<()> {
let svg = create_qr_svg("this is a test QR code \" < > &")?;
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_svg_escaping() {
let svg = inner_generate_secure_join_qr_code(

View File

@@ -11,7 +11,7 @@ use crate::config::Config;
use crate::context::Context;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::session::Session as ImapSession;
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::tools::{self, time_elapsed};
use crate::{stock_str, EventType};
@@ -142,8 +142,8 @@ impl Context {
Some(&highest.to_string()),
)
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::quota_exceeding(self, highest).await;
let mut msg =
Message::new_text(stock_str::quota_exceeding(self, highest).await);
add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
self.set_config_internal(Config::QuotaExceeding, None)

View File

@@ -19,19 +19,20 @@ use std::collections::BTreeMap;
use std::fmt;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::chat::{send_msg, Chat, ChatId};
use crate::chatlist_events;
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
use crate::message::{rfc724_mid_exists, Message, MsgId};
use crate::param::Param;
/// A single reaction consisting of multiple emoji sequences.
///
/// It is guaranteed to have all emojis sorted and deduplicated inside.
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct Reaction {
/// Canonical representation of reaction as a string of space-separated emojis.
reaction: String,
@@ -173,7 +174,7 @@ async fn set_msg_id_reaction(
chat_id: ChatId,
contact_id: ContactId,
timestamp: i64,
reaction: Reaction,
reaction: &Reaction,
) -> Result<()> {
if reaction.is_empty() {
// Simply remove the record instead of setting it to empty string.
@@ -228,8 +229,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
let chat_id = msg.chat_id;
let reaction: Reaction = reaction.into();
let mut reaction_msg = Message::new(Viewtype::Text);
reaction_msg.text = reaction.as_str().to_string();
let mut reaction_msg = Message::new_text(reaction.as_str().to_string());
reaction_msg.set_reaction();
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
reaction_msg.hidden = true;
@@ -244,7 +244,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
msg.chat_id,
ContactId::SELF,
reaction_msg.timestamp_sort,
reaction,
&reaction,
)
.await?;
Ok(reaction_msg_id)
@@ -275,16 +275,28 @@ pub(crate) async fn set_msg_reaction(
contact_id: ContactId,
timestamp: i64,
reaction: Reaction,
is_incoming_fresh: bool,
) -> Result<()> {
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, reaction).await
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
if is_incoming_fresh
&& !reaction.is_empty()
&& msg_id.get_state(context).await?.is_outgoing()
{
context.emit_event(EventType::IncomingReaction {
contact_id,
msg_id,
reaction,
});
}
} else {
info!(
context,
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
);
Ok(())
}
Ok(())
}
/// Get our own reaction for a given message.
@@ -563,6 +575,38 @@ Here's my footer -- bob@example.net"
Ok(())
}
async fn expect_incoming_reactions_event(
t: &TestContext,
expected_msg_id: MsgId,
expected_contact_id: ContactId,
expected_reaction: &str,
) -> Result<()> {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingReaction { .. }))
.await;
match event {
EventType::IncomingReaction {
msg_id,
contact_id,
reaction,
} => {
assert_eq!(msg_id, expected_msg_id);
assert_eq!(contact_id, expected_contact_id);
assert_eq!(reaction, Reaction::from(expected_reaction));
}
_ => unreachable!(),
}
Ok(())
}
async fn has_incoming_reactions_event(t: &TestContext) -> bool {
t.evtracker
.get_matching_opt(t, |evt| matches!(evt, EventType::IncomingReaction { .. }))
.await
.is_some()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -593,6 +637,7 @@ Here's my footer -- bob@example.net"
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
assert!(!has_incoming_reactions_event(&bob).await);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
let bob_reaction_msg = bob.pop_sent_msg().await;
@@ -610,6 +655,7 @@ Here's my footer -- bob@example.net"
assert_eq!(bob_reaction.as_str(), "👍");
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
.await?;
expect_incoming_reactions_event(&alice, alice_msg.sender_msg_id, *bob_id, "👍").await?;
// Alice reacts to own message.
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
@@ -650,6 +696,7 @@ Here's my footer -- bob@example.net"
send_reaction(&bob, bob_msg1.id, "👍").await?;
let bob_send_reaction = bob.pop_sent_msg().await;
alice.recv_msg_trash(&bob_send_reaction).await;
assert!(has_incoming_reactions_event(&alice).await);
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
let summary = chatlist.get_summary(&bob, 0, None).await?;
@@ -665,6 +712,7 @@ Here's my footer -- bob@example.net"
send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?;
let alice_send_reaction = alice.pop_sent_msg().await;
bob.recv_msg_opt(&alice_send_reaction).await;
assert!(!has_incoming_reactions_event(&bob).await);
assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await;
assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await;

View File

@@ -30,7 +30,7 @@ use crate::message::{
};
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::peer_channels::{get_iroh_topic_for_msg, insert_topic_stub, iroh_add_peer_for_topic};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
use crate::peerstate::Peerstate;
use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
@@ -41,7 +41,6 @@ use crate::sync::Sync::*;
use crate::tools::{self, buf_compress, remove_subject_prefix};
use crate::{chatlist_events, location};
use crate::{contact, imap};
use iroh_net::NodeAddr;
/// This is the struct that is returned after receiving one email (aka MIME message).
///
@@ -202,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?;
@@ -357,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
@@ -394,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
@@ -762,6 +760,7 @@ async fn add_parts(
let state: MessageState;
let mut hidden = false;
let mut needs_delete_job = false;
let mut restore_protection = false;
// if contact renaming is prevented (for mailinglists and bots),
// we use name from From:-header as override name
@@ -930,15 +929,11 @@ async fn add_parts(
if chat_id.is_none() {
// try to create a normal chat
let create_blocked = if from_id == ContactId::SELF {
Blocked::Not
} else {
let contact = Contact::get_by_id(context, from_id).await?;
match contact.is_blocked() {
true => Blocked::Yes,
false if is_bot => Blocked::Not,
false => Blocked::Request,
}
let contact = Contact::get_by_id(context, from_id).await?;
let create_blocked = match contact.is_blocked() {
true => Blocked::Yes,
false if is_bot => Blocked::Not,
false => Blocked::Request,
};
if let Some(chat) = test_normal_chat {
@@ -1010,6 +1005,13 @@ async fn add_parts(
)
.await?;
}
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
// verified key, see also `Peerstate::is_using_verified_key()`.
&& contact.is_verified(context).await?;
}
}
}
}
@@ -1032,8 +1034,7 @@ async fn add_parts(
state = MessageState::OutDelivered;
to_id = to_ids.first().copied().unwrap_or_default();
let self_sent =
from_id == ContactId::SELF && to_ids.len() == 1 && to_ids.contains(&ContactId::SELF);
let self_sent = to_ids.len() == 1 && to_ids.contains(&ContactId::SELF);
if mime_parser.sync_items.is_some() && self_sent {
chat_id = Some(DC_CHAT_ID_TRASH);
@@ -1091,8 +1092,8 @@ async fn add_parts(
.await?;
let now = tools::time();
let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::cant_decrypt_outgoing_msgs(context).await;
let mut msg =
Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.log_err(context)
@@ -1422,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)
}
}
}
}
@@ -1442,30 +1447,28 @@ async fn add_parts(
}
if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
match serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address") {
Ok(node_addr) => {
info!(context, "Adding iroh peer with address {node_addr:?}.");
let instance_id = parent.context("Failed to get parent message")?.id;
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
msg_id: instance_id,
});
if let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? {
let node_id = node_addr.node_id;
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server)
.await?;
let iroh = context.get_or_try_init_peer_channel().await?;
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
} else {
chat_id = DC_CHAT_ID_TRASH;
match mime_parser.get_header(HeaderDef::InReplyTo) {
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
Some((instance_id, _ts_sent)) => {
if let Err(err) =
add_gossip_peer_from_header(context, instance_id, node_addr).await
{
warn!(context, "Failed to add iroh peer from header: {err:#}.");
}
}
None => {
warn!(
context,
"Could not add iroh peer because {instance_id} has no topic"
"Cannot add iroh peer because WebXDC instance does not exist."
);
}
chat_id = DC_CHAT_ID_TRASH;
}
Err(err) => {
warn!(context, "Couldn't parse NodeAddr: {err:#}.");
},
None => {
warn!(
context,
"Cannot add iroh peer because the message has no In-Reply-To."
);
}
}
}
@@ -1473,6 +1476,7 @@ async fn add_parts(
for part in &mime_parser.parts {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages;
set_msg_reaction(
context,
mime_in_reply_to,
@@ -1480,6 +1484,7 @@ async fn add_parts(
from_id,
sort_timestamp,
Reaction::from(reaction_str.as_str()),
is_incoming_fresh,
)
.await?;
}
@@ -1505,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)
@@ -1712,7 +1720,16 @@ RETURNING id
// delete it.
needs_delete_job = true;
}
if restore_protection {
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
mime_parser.timestamp_rcvd,
Some(from_id),
)
.await?;
}
Ok(ReceivedMsg {
chat_id,
state,
@@ -2067,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,
@@ -2170,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())
@@ -2651,7 +2679,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);
};
@@ -2724,7 +2752,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

@@ -883,6 +883,54 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_concat_multiple_ndns() -> Result<()> {
let t = TestContext::new().await;
t.configure_addr("alice@posteo.org").await;
let mid = "1234@mail.gmail.com";
receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@posteo.org\n\
To: hanerthaertidiuea@gmx.de\n\
Subject: foo\n\
Message-ID: <1234@mail.gmail.com>\n\
Chat-Version: 1.0\n\
Chat-Disposition-Notification-To: alice@example.org\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
false,
)
.await?;
let chats = Chatlist::try_load(&t, 0, None, None).await?;
let msg_id = chats.get_msg_id(0)?.unwrap();
let raw = include_str!("../../test-data/message/posteo_ndn.eml");
let raw = raw.replace(
"Message-ID: <04422840-f884-3e37-5778-8192fe22d8e1@posteo.de>",
&format!("Message-ID: <{}>", mid),
);
receive_imf(&t, raw.as_bytes(), false).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
let err = "Undelivered Mail Returned to Sender This is the mail system at host mout01.posteo.de.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hanerthaertidiuea@gmx.de>: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)".to_string();
assert_eq!(msg.error(), Some(err.clone()));
assert_eq!(msg.state, MessageState::OutFailed);
let raw = raw.replace(
"Message-Id: <20200609184422.DCB6B1200DD@mout01.posteo.de>",
"Message-Id: <next@mout01.posteo.de>",
);
receive_imf(&t, raw.as_bytes(), false).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.error(), Some([err.clone(), err].join("\n\n")));
Ok(())
}
async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
context
.set_config(Config::ShowEmails, Some("2"))
@@ -2178,8 +2226,7 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> {
let bob = &tcm.bob().await;
bob.set_config_bool(Config::BccSelf, false).await?;
let chat_id = bob.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
msg.text = "Happy birthday to me".to_string();
let mut msg = Message::new_text("Happy birthday to me".to_string());
chat::send_msg(bob, chat_id, &mut msg).await?;
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
Ok(())
@@ -3298,8 +3345,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
assert_eq!(received_group.name, "Group");
assert_eq!(received_group.can_send(&alice1).await?, false); // Can't send because it's Blocked::Request
let mut msg_out = Message::new(Viewtype::Text);
msg_out.set_text("Private reply".to_string());
let mut msg_out = Message::new_text("Private reply".to_string());
assert_eq!(received_group.blocked, Blocked::Request);
msg_out.set_quote(&alice1, Some(&received)).await?;
@@ -3506,8 +3552,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let received_group = Chat::load_from_db(&alice, received.chat_id).await?;
assert_eq!(received_group.typ, Chattype::Group);
let mut msg_out = Message::new(Viewtype::Text);
msg_out.set_text("Private reply".to_string());
let mut msg_out = Message::new_text("Private reply".to_string());
msg_out.set_quote(&alice, Some(&received)).await?;
let alice_bob_chat = alice.create_chat(&bob).await;
@@ -4140,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(())
@@ -4811,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();
@@ -4902,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 {
@@ -655,9 +655,7 @@ async fn fetch_idle(
ctx,
"IMAP session does not support IDLE, going to fake idle."
);
connection
.fake_idle(ctx, &mut session, watch_folder, folder_meaning)
.await?;
connection.fake_idle(ctx, watch_folder).await?;
return Ok(session);
}
@@ -669,9 +667,7 @@ async fn fetch_idle(
.unwrap_or_default()
{
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
connection
.fake_idle(ctx, &mut session, watch_folder, folder_meaning)
.await?;
connection.fake_idle(ctx, watch_folder).await?;
return Ok(session);
}

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

@@ -1,13 +1,13 @@
//! Implementation of [SecureJoin protocols](https://securejoin.delta.chat/).
use anyhow::{bail, ensure, Context as _, Error, Result};
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use anyhow::{ensure, Context as _, Error, Result};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, get_chat_id_by_grpid, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{Blocked, Chattype};
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::ensure_secret_key_exists;
@@ -34,9 +34,6 @@ use qrinvite::QrInvite;
use crate::token::Namespace;
/// Set of characters to percent-encode in email addresses and names.
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
debug_assert!(
progress <= 1000,
@@ -89,12 +86,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
.await?
.unwrap_or_default();
let fingerprint: Fingerprint = match get_self_fingerprint(context).await {
Some(fp) => fp,
None => {
bail!("No fingerprint, cannot generate QR code.");
}
};
let fingerprint = get_self_fingerprint(context).await?;
let self_addr_urlencoded =
utf8_percent_encode(&self_addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
@@ -140,14 +132,11 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
Ok(qr)
}
async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
match load_self_public_key(context).await {
Ok(key) => Some(key.fingerprint()),
Err(_) => {
warn!(context, "get_self_fingerprint(): failed to load key");
None
}
}
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())
}
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
@@ -388,7 +377,7 @@ pub(crate) async fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore);
};
let fingerprint: Fingerprint = fp.parse()?;
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
if !encrypted_and_signed(context, mime_message, &fingerprint) {
warn!(
context,
"Ignoring {step} message because the message is not encrypted."
@@ -577,11 +566,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore);
};
if !encrypted_and_signed(
context,
mime_message,
get_self_fingerprint(context).await.as_ref(),
) {
if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
could_not_establish_secure_connection(
context,
contact_id,
@@ -740,24 +725,19 @@ async fn mark_peer_as_verified(
fn encrypted_and_signed(
context: &Context,
mimeparser: &MimeMessage,
expected_fingerprint: Option<&Fingerprint>,
expected_fingerprint: &Fingerprint,
) -> bool {
if !mimeparser.was_encrypted() {
warn!(context, "Message not encrypted.",);
false
} else if let Some(expected_fingerprint) = expected_fingerprint {
if !mimeparser.signatures.contains(expected_fingerprint) {
warn!(
context,
"Message does not match expected fingerprint {}.", expected_fingerprint,
);
false
} else {
true
}
} else {
warn!(context, "Fingerprint for comparison missing.");
} else if !mimeparser.signatures.contains(expected_fingerprint) {
warn!(
context,
"Message does not match expected fingerprint {}.", expected_fingerprint,
);
false
} else {
true
}
}
@@ -785,6 +765,7 @@ mod tests {
WrongAliceGossip,
SecurejoinWaitTimeout,
AliceIsBot,
AliceHasName,
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -812,10 +793,21 @@ mod tests {
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_alice_has_name() {
test_setup_contact_ex(SetupContactCase::AliceHasName).await
}
async fn test_setup_contact_ex(case: SetupContactCase) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
if case == SetupContactCase::AliceHasName {
alice
.set_config(Config::Displayname, Some("Alice"))
.await
.unwrap();
}
let bob = tcm.bob().await;
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
.await
@@ -860,7 +852,10 @@ mod tests {
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let sent = bob.pop_sent_msg().await;
assert!(!sent.payload.contains("Bob Examplenet"));
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
@@ -994,6 +989,7 @@ mod tests {
.await
.unwrap();
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
assert!(contact_bob.get_name().is_empty());
assert_eq!(contact_bob.is_bot(), false);
// exactly one one-to-one chat should be visible for both now
@@ -1023,14 +1019,13 @@ mod tests {
}
// Make sure Alice hasn't yet sent their name to Bob.
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(contact_alice.get_authname(), "");
match case {
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
_ => assert_eq!(contact_alice.get_authname(), ""),
};
// Check Alice sent the right message to Bob.
let sent = alice.pop_sent_msg().await;
@@ -1053,6 +1048,7 @@ mod tests {
.await
.unwrap();
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
assert!(contact_alice.get_name().is_empty());
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
if case != SetupContactCase::SecurejoinWaitTimeout {

View File

@@ -270,7 +270,7 @@ impl BobState {
context,
"Bob Step 4 - handling {{vc,vg}}-auth-required message."
);
if !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint())) {
if !encrypted_and_signed(context, mime_message, self.invite.fingerprint()) {
let reason = if mime_message.was_encrypted() {
"Valid signature missing"
} else {

View File

@@ -21,7 +21,9 @@ pub fn escape_message_footer_marks(text: &str) -> String {
/// `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 {
@@ -296,7 +298,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 +403,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

@@ -126,6 +126,7 @@ impl Smtp {
let login_params =
prioritize_server_login_params(&context.sql, login_params, "smtp").await?;
let mut first_error = None;
for lp in login_params {
info!(context, "SMTP trying to connect to {}.", &lp.connection);
let transport = match connect::connect_and_auth(
@@ -143,6 +144,7 @@ impl Smtp {
Ok(transport) => transport,
Err(err) => {
warn!(context, "SMTP failed to connect and authenticate: {err:#}.");
first_error.get_or_insert(err);
continue;
}
};
@@ -157,7 +159,7 @@ impl Smtp {
return Ok(());
}
Err(format_err!("SMTP failed to connect"))
Err(first_error.unwrap_or_else(|| format_err!("No SMTP connection candidates provided")))
}
}
@@ -355,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| {
@@ -368,10 +370,15 @@ pub(crate) async fn send_msg_to_smtp(
Ok((mime, recipients, msg_id, retries))
},
)
.await?;
.await?
else {
return Ok(());
};
if retries > 6 {
let mut msg = Message::load_from_db(context, msg_id).await?;
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.").await?;
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.")
.await?;
}
context
.sql
.execute("DELETE FROM smtp WHERE id=?", (rowid,))

View File

@@ -168,6 +168,7 @@ async fn connect_stream(
connect_insecure_proxy(context, host, port, proxy_config.clone()).await?
}
};
update_connection_history(context, "smtp", host, port, host, time()).await?;
Ok(stream)
} else {
let load_cache = match security {

View File

@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use anyhow::{bail, Context as _, Result};
use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row};
use tokio::sync::{Mutex, MutexGuard, RwLock};
use tokio::sync::RwLock;
use crate::blob::BlobObject;
use crate::chat::{self, add_device_msg, update_device_icon, update_saved_messages_icon};
@@ -17,7 +17,7 @@ use crate::ephemeral::start_ephemeral_timers;
use crate::imex::BLOBS_BACKUP_NAME;
use crate::location::delete_orphaned_poi_locations;
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::message::{Message, MsgId};
use crate::net::dns::prune_dns_cache;
use crate::net::prune_connection_history;
use crate::param::{Param, Params};
@@ -60,11 +60,6 @@ pub struct Sql {
/// Database file path
pub(crate) dbfile: PathBuf,
/// Write transactions mutex.
///
/// See [`Self::write_lock`].
write_mtx: Mutex<()>,
/// SQL connection pool.
pool: RwLock<Option<Pool>>,
@@ -81,7 +76,6 @@ impl Sql {
pub fn new(dbfile: PathBuf) -> Sql {
Self {
dbfile,
write_mtx: Mutex::new(()),
pool: Default::default(),
is_encrypted: Default::default(),
config_cache: Default::default(),
@@ -147,7 +141,8 @@ impl Sql {
let mut config_cache = self.config_cache.write().await;
config_cache.clear();
self.call_write(move |conn| {
let query_only = false;
self.call(query_only, move |conn| {
// Check that backup passphrase is correct before resetting our database.
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
.context("failed to attach backup database")?;
@@ -159,29 +154,28 @@ impl Sql {
// don't have main database passphrase at this point.
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
// Without resetting import may fail due to existing tables.
let res = res.and_then(|_| {
res.and_then(|_| {
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")
});
let res = res.and_then(|_| {
})
.and_then(|_| {
conn.execute("VACUUM", [])
.context("failed to vacuum the database")
});
let res = res.and(
})
.and(
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE"),
);
let res = res.and_then(|_| {
)
.and_then(|_| {
conn.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
Ok(())
})
.context("failed to import from attached backup database")
});
let res = res.and(
})
.and(
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database"),
);
res?;
)?;
Ok(())
})
.await
@@ -252,8 +246,7 @@ impl Sql {
// We now always watch all folders and delete messages there if delete_server is enabled.
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
if context.get_config_delete_server_after().await?.is_some() {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(stock_str::delete_server_turned_off(context).await);
let mut msg = Message::new_text(stock_str::delete_server_turned_off(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
context
.set_config_internal(Config::DeleteServerAfter, Some("0"))
@@ -325,7 +318,8 @@ impl Sql {
let mut lock = self.pool.write().await;
let pool = lock.take().context("SQL connection pool is not open")?;
let conn = pool.get().await?;
let query_only = false;
let conn = pool.get(query_only).await?;
if !passphrase.is_empty() {
conn.pragma_update(None, "rekey", passphrase.clone())
.context("Failed to set PRAGMA rekey")?;
@@ -337,59 +331,20 @@ impl Sql {
Ok(())
}
/// Locks the write transactions mutex in order to make sure that there never are
/// multiple write transactions at once.
/// Allocates a connection and calls `function` with the connection.
///
/// Doing the locking ourselves instead of relying on SQLite has these reasons:
///
/// - SQLite's locking mechanism is non-async, blocking a thread
/// - SQLite's locking mechanism just sleeps in a loop, which is really inefficient
///
/// ---
///
/// More considerations on alternatives to the current approach:
///
/// We use [DEFERRED](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions) transactions.
///
/// In order to never get concurrency issues, we could make all transactions IMMEDIATE,
/// but this would mean that there can never be two simultaneous transactions.
///
/// Read transactions can simply be made DEFERRED to run in parallel w/o any drawbacks.
///
/// DEFERRED write transactions without doing the locking ourselves would have these drawbacks:
///
/// 1. As mentioned above, SQLite's locking mechanism is non-async and sleeps in a loop.
/// 2. If there are other write transactions, we block the db connection until
/// upgraded. If some reader comes then, it has to get the next, less used connection with a
/// worse per-connection page cache (SQLite allows one write and any number of reads in parallel).
/// 3. If a transaction is blocked for more than `busy_timeout`, it fails with SQLITE_BUSY.
/// 4. If upon a successful upgrade to a write transaction the db has been modified,
/// the transaction has to be rolled back and retried, which means extra work in terms of
/// CPU/battery.
///
/// The only pro of making write transactions DEFERRED w/o the external locking would be some
/// parallelism between them.
///
/// Another option would be to make write transactions IMMEDIATE, also
/// w/o the external locking. But then cons 1. - 3. above would still be valid.
pub async fn write_lock(&self) -> MutexGuard<'_, ()> {
self.write_mtx.lock().await
}
/// Allocates a connection and calls `function` with the connection. If `function` does write
/// queries,
/// - either first take a lock using `write_lock()`
/// - or use `call_write()` instead.
/// If `query_only` is true, allocates read-only connection,
/// otherwise allocates write connection.
///
/// Returns the result of the function.
async fn call<'a, F, R>(&'a self, function: F) -> Result<R>
async fn call<'a, F, R>(&'a self, query_only: bool, function: F) -> Result<R>
where
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
R: Send + 'static,
{
let lock = self.pool.read().await;
let pool = lock.as_ref().context("no SQL connection")?;
let mut conn = pool.get().await?;
let mut conn = pool.get(query_only).await?;
let res = tokio::task::block_in_place(move || function(&mut conn))?;
Ok(res)
}
@@ -403,8 +358,8 @@ impl Sql {
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
R: Send + 'static,
{
let _lock = self.write_lock().await;
self.call(function).await
let query_only = false;
self.call(query_only, function).await
}
/// Execute `query` assuming it is a write query, returning the number of affected rows.
@@ -444,7 +399,8 @@ impl Sql {
G: Send + FnMut(rusqlite::MappedRows<F>) -> Result<H>,
H: Send + 'static,
{
self.call(move |conn| {
let query_only = true;
self.call(query_only, move |conn| {
let mut stmt = conn.prepare(sql)?;
let res = stmt.query_map(params, f)?;
g(res)
@@ -476,7 +432,8 @@ impl Sql {
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T> + Send,
T: Send + 'static,
{
self.call(move |conn| {
let query_only = true;
self.call(query_only, move |conn| {
let res = conn.query_row(query, params, f)?;
Ok(res)
})
@@ -512,7 +469,8 @@ impl Sql {
/// Query the database if the requested table already exists.
pub async fn table_exists(&self, name: &str) -> Result<bool> {
self.call(move |conn| {
let query_only = true;
self.call(query_only, move |conn| {
let mut exists = false;
conn.pragma(None, "table_info", name.to_string(), |_row| {
// will only be executed if the info was found
@@ -527,7 +485,8 @@ impl Sql {
/// Check if a column exists in a given table.
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> Result<bool> {
self.call(move |conn| {
let query_only = true;
self.call(query_only, move |conn| {
let mut exists = false;
// `PRAGMA table_info` returns one row per column,
// each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value
@@ -555,10 +514,13 @@ impl Sql {
F: Send + FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
T: Send + 'static,
{
self.call(move |conn| match conn.query_row(sql.as_ref(), params, f) {
Ok(res) => Ok(Some(res)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(err) => Err(err.into()),
let query_only = true;
self.call(query_only, move |conn| {
match conn.query_row(sql.as_ref(), params, f) {
Ok(res) => Ok(Some(res)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(err) => Err(err.into()),
}
})
.await
}
@@ -717,6 +679,36 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
Ok(conn)
}
// Tries to clear the freelist to free some space on the disk.
//
// This only works if auto_vacuum is enabled.
async fn incremental_vacuum(context: &Context) -> Result<()> {
context
.sql
.call_write(move |conn| {
let mut stmt = conn
.prepare("PRAGMA incremental_vacuum")
.context("Failed to prepare incremental_vacuum statement")?;
// It is important to step the statement until it returns no more rows.
// Otherwise it will not free as many pages as it can:
// <https://stackoverflow.com/questions/53746807/sqlite-incremental-vacuum-removing-only-one-free-page>.
let mut rows = stmt
.query(())
.context("Failed to run incremental_vacuum statement")?;
let mut row_count = 0;
while let Some(_row) = rows
.next()
.context("Failed to step incremental_vacuum statement")?
{
row_count += 1;
}
info!(context, "Incremental vacuum freed {row_count} pages.");
Ok(())
})
.await
}
/// Cleanup the account to restore some storage and optimize the database.
pub async fn housekeeping(context: &Context) -> Result<()> {
// Setting `Config::LastHousekeeping` at the beginning avoids endless loops when things do not
@@ -749,24 +741,8 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
);
}
// Try to clear the freelist to free some space on the disk. This
// only works if auto_vacuum is enabled.
match context
.sql
.query_row_optional("PRAGMA incremental_vacuum", (), |_row| Ok(()))
.await
{
Err(err) => {
warn!(context, "Failed to run incremental vacuum: {err:#}.");
}
Ok(Some(())) => {
// Incremental vacuum returns a zero-column result if it did anything.
info!(context, "Successfully ran incremental vacuum.");
}
Ok(None) => {
// Incremental vacuum returned `SQLITE_DONE` immediately,
// there were no pages to remove.
}
if let Err(err) = incremental_vacuum(context).await {
warn!(context, "Failed to run incremental vacuum: {err:#}.");
}
context
@@ -1092,9 +1068,10 @@ mod tests {
async fn test_auto_vacuum() -> Result<()> {
let t = TestContext::new().await;
let query_only = true;
let auto_vacuum = t
.sql
.call(|conn| {
.call(query_only, |conn| {
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
let auto_vacuum: i32 = row.get(0)?;
Ok(auto_vacuum)
@@ -1150,8 +1127,7 @@ mod tests {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
let mut new_draft = Message::new(Viewtype::Text);
new_draft.set_text("This is my draft".to_string());
let mut new_draft = Message::new_text("This is my draft".to_string());
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
housekeeping(&t).await.unwrap();
@@ -1320,8 +1296,9 @@ mod tests {
{
let lock = sql.pool.read().await;
let pool = lock.as_ref().unwrap();
let conn1 = pool.get().await?;
let conn2 = pool.get().await?;
let query_only = true;
let conn1 = pool.get(query_only).await?;
let conn2 = pool.get(query_only).await?;
conn1
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.unwrap();
@@ -1346,4 +1323,72 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_query_only() -> Result<()> {
let t = TestContext::new().await;
// `query_row` does not acquire write lock
// and operates on read-only connection.
// Using it to `INSERT` should fail.
let res = t
.sql
.query_row(
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1",
("xyz", "ijk"),
|row| {
let res: u32 = row.get(0)?;
Ok(res)
},
)
.await;
assert!(res.is_err());
// If you want to `INSERT` and get value via `RETURNING`,
// use `call_write` or `transaction`.
let res: Result<u32> = t
.sql
.call_write(|conn| {
let val = conn.query_row(
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2",
("foo", "bar"),
|row| {
let res: u32 = row.get(0)?;
Ok(res)
},
)?;
Ok(val)
})
.await;
assert_eq!(res.unwrap(), 2);
let res = t
.sql
.transaction(|t| {
let val = t.query_row(
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3",
("abc", "def"),
|row| {
let res: u32 = row.get(0)?;
Ok(res)
},
)?;
Ok(val)
})
.await;
assert_eq!(res.unwrap(), 3);
Ok(())
}
/// Tests that incremental_vacuum does not fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_incremental_vacuum() -> Result<()> {
let t = TestContext::new().await;
incremental_vacuum(&t).await?;
Ok(())
}
}

View File

@@ -7,23 +7,67 @@
//! Each SQLite connection has its own page cache, so allocating recently used connections
//! improves the performance compared to, for example, organizing the pool as a queue
//! and returning the least recently used connection each time.
//!
//! Pool returns at most one write connection (with `PRAGMA query_only=0`).
//! This ensures that there never are multiple write transactions at once.
//!
//! Doing the locking ourselves instead of relying on SQLite has these reasons:
//!
//! - SQLite's locking mechanism is non-async, blocking a thread
//! - SQLite's locking mechanism just sleeps in a loop, which is really inefficient
//!
//! ---
//!
//! More considerations on alternatives to the current approach:
//!
//! We use [DEFERRED](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions) transactions.
//!
//! In order to never get concurrency issues, we could make all transactions IMMEDIATE,
//! but this would mean that there can never be two simultaneous transactions.
//!
//! Read transactions can simply be made DEFERRED to run in parallel w/o any drawbacks.
//!
//! DEFERRED write transactions without doing the locking ourselves would have these drawbacks:
//!
//! 1. As mentioned above, SQLite's locking mechanism is non-async and sleeps in a loop.
//! 2. If there are other write transactions, we block the db connection until
//! upgraded. If some reader comes then, it has to get the next, less used connection with a
//! worse per-connection page cache (SQLite allows one write and any number of reads in parallel).
//! 3. If a transaction is blocked for more than `busy_timeout`, it fails with SQLITE_BUSY.
//! 4. If upon a successful upgrade to a write transaction the db has been modified,
//! the transaction has to be rolled back and retried, which means extra work in terms of
//! CPU/battery.
//!
//! The only pro of making write transactions DEFERRED w/o the external locking would be some
//! parallelism between them.
//!
//! Another option would be to make write transactions IMMEDIATE, also
//! w/o the external locking. But then cons 1. - 3. above would still be valid.
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Weak};
use anyhow::{Context, Result};
use parking_lot::Mutex;
use rusqlite::Connection;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use tokio::sync::{Mutex, OwnedMutexGuard, OwnedSemaphorePermit, Semaphore};
/// Inner connection pool.
#[derive(Debug)]
struct InnerPool {
/// Available connections.
connections: Mutex<Vec<Connection>>,
connections: parking_lot::Mutex<Vec<Connection>>,
/// Counts the number of available connections.
semaphore: Arc<Semaphore>,
/// Write mutex.
///
/// This mutex ensures there is at most
/// one write connection with `query_only=0`.
///
/// This mutex is locked when write connection
/// is outside the pool.
write_mutex: Arc<Mutex<()>>,
}
impl InnerPool {
@@ -35,6 +79,56 @@ impl InnerPool {
connections.push(connection);
drop(connections);
}
/// Retrieves a connection from the pool.
///
/// Sets `query_only` pragma to the provided value
/// to prevent accidentaly misuse of connection
/// for writing when reading is intended.
/// Only pass `query_only=false` if you want
/// to use the connection for writing.
pub async fn get(self: Arc<Self>, query_only: bool) -> Result<PooledConnection> {
if query_only {
let permit = self.semaphore.clone().acquire_owned().await?;
let conn = {
let mut connections = self.connections.lock();
connections
.pop()
.context("Got a permit when there are no connections in the pool")?
};
conn.pragma_update(None, "query_only", "1")?;
let conn = PooledConnection {
pool: Arc::downgrade(&self),
conn: Some(conn),
_permit: permit,
_write_mutex_guard: None,
};
Ok(conn)
} else {
// We get write guard first to avoid taking a permit
// and not using it, blocking a reader from getting a connection
// while being ourselves blocked by another wrtier.
let write_mutex_guard = Arc::clone(&self.write_mutex).lock_owned().await;
// We may still have to wait for a connection
// to be returned by some reader.
let permit = self.semaphore.clone().acquire_owned().await?;
let conn = {
let mut connections = self.connections.lock();
connections.pop().context(
"Got a permit and write lock when there are no connections in the pool",
)?
};
conn.pragma_update(None, "query_only", "0")?;
let conn = PooledConnection {
pool: Arc::downgrade(&self),
conn: Some(conn),
_permit: permit,
_write_mutex_guard: Some(write_mutex_guard),
};
Ok(conn)
}
}
}
/// Pooled connection.
@@ -47,6 +141,11 @@ pub struct PooledConnection {
/// Semaphore permit, dropped after returning the connection to the pool.
_permit: OwnedSemaphorePermit,
/// Write mutex guard.
///
/// `None` for read-only connections with `PRAGMA query_only=1`.
_write_mutex_guard: Option<OwnedMutexGuard<()>>,
}
impl Drop for PooledConnection {
@@ -86,24 +185,14 @@ impl Pool {
pub fn new(connections: Vec<Connection>) -> Self {
let semaphore = Arc::new(Semaphore::new(connections.len()));
let inner = Arc::new(InnerPool {
connections: Mutex::new(connections),
connections: parking_lot::Mutex::new(connections),
semaphore,
write_mutex: Default::default(),
});
Pool { inner }
}
/// Retrieves a connection from the pool.
pub async fn get(&self) -> Result<PooledConnection> {
let permit = self.inner.semaphore.clone().acquire_owned().await?;
let mut connections = self.inner.connections.lock();
let conn = connections
.pop()
.context("got a permit when there are no connections in the pool")?;
let conn = PooledConnection {
pool: Arc::downgrade(&self.inner),
conn: Some(conn),
_permit: permit,
};
Ok(conn)
pub async fn get(&self, query_only: bool) -> Result<PooledConnection> {
Arc::clone(&self.inner).get(query_only).await
}
}

View File

@@ -1419,8 +1419,7 @@ impl Context {
msg.param.set(Param::File, blob.as_name());
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = welcome_message(self).await;
let mut msg = Message::new_text(welcome_message(self).await);
chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
Ok(())
}

View File

@@ -305,8 +305,7 @@ mod tests {
.unwrap();
let some_text = " bla \t\n\tbla\n\t".to_string();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(some_text.to_string());
let msg = Message::new_text(some_text.to_string());
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
let mut msg = Message::new(Viewtype::Image);
@@ -415,8 +414,7 @@ mod tests {
}
// Forwarded
let mut msg = Message::new(Viewtype::Text);
msg.set_text(some_text.clone());
let mut msg = Message::new_text(some_text.clone());
msg.param.set_int(Param::Forwarded, 1);
assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); // for simple text, the type is not added to the summary
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); // skipping prefix used for reactions summaries

View File

@@ -730,8 +730,7 @@ impl TestContext {
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
/// the message.
pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage<'_> {
let mut msg = Message::new(Viewtype::Text);
msg.text = txt.to_string();
let mut msg = Message::new_text(txt.to_string());
self.send_msg(chat_id, &mut msg).await
}
@@ -1106,6 +1105,24 @@ impl EventTracker {
.expect("timeout waiting for event match")
}
/// Consumes emitted events returning the first matching one if any.
pub async fn get_matching_opt<F: Fn(&EventType) -> bool>(
&self,
ctx: &Context,
event_matcher: F,
) -> Option<EventType> {
ctx.emit_event(EventType::Test);
loop {
let event = self.recv().await.unwrap();
if event_matcher(&event.typ) {
return Some(event.typ);
}
if let EventType::Test = event.typ {
return None;
}
}
}
/// Consumes events looking for an [`EventType::Info`] with substring matching.
pub async fn get_info_contains(&self, s: &str) -> EventType {
self.get_matching(|evt| match evt {

View File

@@ -6,7 +6,7 @@ use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING};
use crate::contact::{Contact, ContactId, Origin};
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
@@ -297,6 +297,7 @@ async fn test_verified_oneonone_chat_enable_disable() -> Result<()> {
assert!(chat.is_protected());
for alice_accepts_breakage in [true, false] {
SystemTime::shift(std::time::Duration::from_secs(300));
// Bob uses Thunderbird to send a message
receive_imf(
&alice,
@@ -715,8 +716,7 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
assert!(!alice_bob_chat.can_send(&alice).await?);
// Alice's UI should still be able to save a draft, which Alice started to type right when she got Bob's message:
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Draftttt".to_string());
let mut msg = Message::new_text("Draftttt".to_string());
alice_bob_chat.id.set_draft(&alice, Some(&mut msg)).await?;
assert_eq!(
alice_bob_chat.id.get_draft(&alice).await?.unwrap().text,
@@ -730,6 +730,50 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_message_from_old_dc_setup() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob_old = &tcm.unconfigured().await;
enable_verified_oneonone_chats(&[alice, bob_old]).await;
mark_as_verified(bob_old, alice).await;
bob_old.configure_addr("bob@example.net").await;
let chat = bob_old.create_chat(alice).await;
let sent_old = bob_old
.send_text(chat.id, "Soon i'll have a new device")
.await;
SystemTime::shift(std::time::Duration::from_secs(3600));
tcm.section("Bob reinstalls DC");
let bob = &tcm.bob().await;
enable_verified_oneonone_chats(&[bob]).await;
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
tcm.send_recv(bob, alice, "Now i have it!").await;
assert_verified(alice, bob, ProtectionStatus::Protected).await;
let msg = alice.recv_msg(&sent_old).await;
assert!(!msg.get_showpadlock());
let contact = alice.add_or_lookup_contact(bob).await;
// The outdated Bob's Autocrypt header isn't applied, so the verification preserves.
assert!(contact.is_verified(alice).await.unwrap());
let chat = alice.get_chat(bob).await;
assert!(chat.is_protected());
assert_eq!(chat.is_protection_broken(), false);
let protection_msg = alice.get_last_msg().await;
assert_eq!(
protection_msg.param.get_cmd(),
SystemMessage::ChatProtectionEnabled
);
assert!(protection_msg.timestamp_sort >= msg.timestamp_rcvd);
alice
.golden_test_chat(msg.chat_id, "verified_chats_message_from_old_dc_setup")
.await;
Ok(())
}
/// Regression test for the following bug:
///
/// - Scan your chat partner's QR Code

View File

@@ -250,8 +250,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) {
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::update_reminder_msg_body(context).await;
let mut msg = Message::new_text(stock_str::update_reminder_msg_body(context).await);
if let Some(timestamp) = chrono::DateTime::<chrono::Utc>::from_timestamp(now, 0) {
add_device_msg(
context,

View File

@@ -36,7 +36,7 @@ use crate::{chat, location};
use std::collections::{hash_map, HashMap};
use crate::context::Context;
use crate::message::{Message, MsgId, Viewtype};
use crate::message::{Message, MsgId};
use crate::chat::ChatId;
use crate::color::color_int_to_hex_string;
@@ -85,8 +85,7 @@ pub(crate) async fn intercept_send_update(
ChatId::create_for_contact(context, ContactId::SELF).await?
};
let mut poi_msg = Message::new(Viewtype::Text);
poi_msg.text = label;
let mut poi_msg = Message::new_text(label);
poi_msg.set_location(lat, lng);
chat::send_msg(context, chat_id, &mut poi_msg).await?;
} else {

View File

@@ -1,7 +1,8 @@
Single#Chat#10: Bob [bob@example.net]
Single#Chat#10: Bob [bob@example.net] 🛡️
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11🔒: (Contact#Contact#10): Heyho from my verified device! [FRESH]
Msg#12: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#13: (Contact#Contact#10): Old, unverified message [SEEN]
Msg#14: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
--------------------------------------------------------------------------------

View File

@@ -0,0 +1,8 @@
Single#Chat#10: bob@example.net [bob@example.net] 🛡️
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11🔒: (Contact#Contact#10): Now i have it! [FRESH]
Msg#12: info (Contact#Contact#Info): bob@example.net sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#13: (Contact#Contact#10): Soon i'll have a new device [FRESH]
Msg#14: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
--------------------------------------------------------------------------------