Compare commits

...

40 Commits

Author SHA1 Message Date
Hocuri
ff33044e3b fix: Make "end-to-end encryption not setup" message work again
Reported by adbenitez in the release coordination group:

The "end-to-end encryption not setup" message didn't appear anymore when
trying to send an unencrypted message with a chatmail account. This was
because on the server side, the casing of the error message changed
("Inalid unencrypted mail" -> "Invalid Unencrypted mail").

This PR ignores the casing.
2025-08-05 15:43:58 +02:00
dependabot[bot]
2d798f7cfe Merge pull request #7066 from chatmail/dependabot/cargo/toml-0.9.4 2025-08-04 23:53:05 +00:00
link2xt
08bb0484eb chore(release): prepare for 2.10.0 2025-08-04 22:33:59 +00:00
link2xt
b0b7337f5a chore: upgrade async-imap to 0.11.1 2025-08-04 21:42:28 +00:00
Hocuri
93241a4beb feat: Also lookup key contacts in lookup_id_by_addr() (#7073)
If there is both a key and an address contact, return the most recently
seen one.
2025-08-04 21:32:09 +02:00
iequidoo
4f1bf1f13c chore(deny.toml): add exception for duplicate toml_datetime 0.6.11 dependency 2025-08-02 16:16:43 -03:00
iequidoo
2d0b7b5bd8 chore(cargo): bump human-panic from 2.0.2 to 2.0.3 2025-08-02 16:14:48 -03:00
dependabot[bot]
8fe3ce5cab chore(cargo): bump strum_macros from 0.27.1 to 0.27.2
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.27.1 to 0.27.2.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/compare/v0.27.1...v0.27.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 14:15:55 -03:00
dependabot[bot]
59a0f1d94f chore(cargo): bump strum from 0.27.1 to 0.27.2
Bumps [strum](https://github.com/Peternator7/strum) from 0.27.1 to 0.27.2.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/compare/v0.27.1...v0.27.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:55:09 -03:00
dependabot[bot]
5175dc3450 chore(cargo): bump criterion from 0.6.0 to 0.7.0
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.6.0 to 0.7.0.
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.6.0...0.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:39:05 -03:00
dependabot[bot]
9a22ccd058 chore(cargo): bump hyper-util from 0.1.14 to 0.1.16
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.14 to 0.1.16.
- [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.14...v0.1.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:38:26 -03:00
dependabot[bot]
c06ed49a2a chore(cargo): bump async-channel from 2.3.1 to 2.5.0
Bumps [async-channel](https://github.com/smol-rs/async-channel) from 2.3.1 to 2.5.0.
- [Release notes](https://github.com/smol-rs/async-channel/releases)
- [Changelog](https://github.com/smol-rs/async-channel/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-channel/compare/v2.3.1...v2.5.0)

---
updated-dependencies:
- dependency-name: async-channel
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:33:11 -03:00
dependabot[bot]
2e51a5a454 chore(cargo): bump bolero from 0.13.3 to 0.13.4
Bumps [bolero](https://github.com/camshaft/bolero) from 0.13.3 to 0.13.4.
- [Changelog](https://github.com/camshaft/bolero/blob/master/CHANGELOG.md)
- [Commits](https://github.com/camshaft/bolero/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 12:59:56 -03:00
dependabot[bot]
75cc353528 chore(cargo): bump serde_json from 1.0.140 to 1.0.142
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.140 to 1.0.142.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.140...v1.0.142)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 12:51:47 -03:00
dependabot[bot]
3977580426 chore(cargo): bump toml from 0.8.23 to 0.9.4
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.23 to 0.9.4.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v0.9.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-01 22:00:13 +00:00
link2xt
3a1370e174 chore(release): prepare for 2.9.0 2025-07-31 18:57:58 +00:00
iequidoo
c218c05b96 fix: get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker
We were reporting the UTC midnight timestamp instead. For UTC-N timezones that means reporting
"yesterday".

Fixes https://github.com/deltachat/deltachat-desktop/issues/5215.
2025-07-31 13:55:28 -03:00
iequidoo
db247d9f9a refactor: Don't call add_or_lookup_key_contacts() in advance
Its result isn't needed in all the branches.
2025-07-31 12:07:58 -03:00
iequidoo
78b7715ea6 refactor: Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts()
- It's obvious that addresses are needed to add contacts, so `_by_address_list` looks excessive.
- The function only looks up `SELF` by address.
- The name was confusing because there's also `lookup_key_contacts_by_address_list()` that actually
  looks up key contacts by addresses (not only `SELF`).
2025-07-31 12:07:58 -03:00
link2xt
ba76944d75 fix: display correct timer value for ephemeral timer changes
The timer change should not disappear,
but should display correct timer change.
2025-07-31 15:00:08 +00:00
cliffmccarthy
4a1a2122f0 feat(repl): Add import-vcard and make-vcard commands (#7048)
- Added import-vcard and make-vcard commands to deltachat-repl. These
  wrap the corresponding import_vcard() and make_vcard() operations from
  src/contact.rs. Each command takes a file path argument specifying the
  location of the vCard file to be read or written. The make-vcard command
  takes one or more IDs to write to the file.
- Documented commands in "Using the CLI client" section of README.md.

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-07-30 13:16:29 -03:00
link2xt
d80b749dec chore(release): prepare for 2.8.0 2025-07-28 19:31:43 +00:00
link2xt
039a8b7c36 fix: lookup self by address if there is no fingerprint or gossip 2025-07-28 19:18:07 +00:00
iequidoo
779f58ab16 feat: Remove ProtectionBroken, make such chats Unprotected (#7041)
Chats can't break anymore.
2025-07-28 16:01:14 -03:00
link2xt
b9183fe5eb chore(release): prepare for 2.7.0 2025-07-26 22:49:44 +00:00
Hocuri
9d342671d5 fix: Do not fail to upgrade if the verifier of a contact doesn't exist anymore (#7044)
Fix https://github.com/chatmail/core/issues/7043
2025-07-26 22:41:11 +02:00
link2xt
4e47ebd5fc test: fix flaky test_webxdc_resend 2025-07-24 17:47:19 +00:00
Hocuri
d5c418e909 feat: Put the debug/release build version into the info (#7034)
The info will look like:

```
debug_assertions=On - DO NOT RELEASE THIS BUILD
```
or:
```
debug_assertions=Off
```

I tested that this actually works when compiling on Android.

<details><summary>This is how it looked before the second
commit</summary>
<p>


The deltachat_core_version line in the info will look like:

```
deltachat_core_version=v2.5.0 [debug build]
```
or:
```
deltachat_core_version=v2.5.0 [release build]
```

I tested that this actually works when compiling on Android.



</p>
</details>
2025-07-24 17:24:54 +02:00
Hocuri
85414558c5 test: Add regression test for verification-gossiping crash (#7033)
This adds a test for https://github.com/chatmail/core/pull/7032/.

The crash happened if you received a message that has the from contact
also in the "To: " and "Chat-Group-Member-Fpr: " headers. Not sure how
it happened that such a message was created; I managed to create one in
a test, but needed to access some internals of MimeFactory for that.

I then saved the email that is sent by Alice into the test-data
directory, and then made a test that Bob doesn't crash if he receives
this email. And as a sanity-check, also test that Bob marks Fiona as
verified after receiving this email.
2025-07-24 17:24:00 +02:00
iequidoo
d6af8d2526 feat: mimefactory: Order message recipients by time of addition (#6872)
Sort recipients by `add_timestamp DESC` so that if the group is large and there are multiple SMTP
messages, a newly added member receives the member addition message earlier and has gossiped keys of
other members (otherwise the new member may receive messages from other members earlier and fail to
verify them).
2025-07-24 03:11:46 -03:00
Sebastian Klähn
1209e95e34 fix: realtime late join (#6869)
This PR adds a test to reproduce a bug raised by @adbenitez that peer
channels break when the resend feature is used.

---------

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-07-23 12:50:53 +02:00
Hocuri
51f9279e67 chore(release): prepare for 2.6.0 2025-07-23 11:47:05 +02:00
Hocuri
f27d54f7fa fix: Fix crash when receiving a verification-gossiping message which a contact also sends to itself (#7032)
This should fix https://github.com/chatmail/core/issues/7030; @r10s if
you could test whether it fixes your problem

The crash happened if you received a message that has the from contact
also in the "To: " and "Chat-Group-Member-Fpr: " headers. Not sure how
it happened that such a message was created.
2025-07-23 11:09:11 +02:00
link2xt
7f3648f8ae chore(release): prepare for 2.5.0 2025-07-22 14:21:07 +00:00
link2xt
49fc258578 fix: do not ignore errors in add_flag_finalized_with_set 2025-07-22 14:17:30 +00:00
link2xt
0c51b4fe41 docs(STYLE.md): prefer try_next() over next() 2025-07-22 12:33:37 +00:00
bjoern
dbad714539 docs: clarify the meaning of is_verified() vs verifier_id() (#7027)
this PR adapts the documentation UI guidance to recent "green checkmark"
discussions

cmp https://github.com/deltachat/deltachat-pages/pull/1145,
https://github.com/deltachat/deltachat-ios/pull/2781,
https://github.com/deltachat/deltachat-android/pull/3828,
https://github.com/deltachat/deltachat-desktop/pull/5318

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-22 10:40:12 +02:00
Hocuri
edd8008650 fix: Mark all email chats as unprotected in the migration (#7026)
Previously, chat protection was only removed if the chat became an
email-chat because the key-contact has a different email, but not if the
chat became an email-chat for a different reason.

Therefore, it could happen that the migration produced a protected,
unencrypted chat, where you couldn't write any messages.

I tested that applying this fix actually fixes the bug I had.
2025-07-21 20:15:59 +00:00
Hocuri
615a1b3f4e fix: Correctly migrate "verified by me" 2025-07-21 18:28:19 +00:00
bjoern
fe6044e1aa docs: deprecate protection-broken and related stuff (#7018)
came over these parts while targeting the new info message of
https://github.com/chatmail/core/pull/7008 in
https://github.com/deltachat/deltachat-ios/pull/2778 and
https://github.com/deltachat/deltachat-android/pull/3822

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-21 18:40:00 +02:00
47 changed files with 995 additions and 336 deletions

View File

@@ -1,5 +1,84 @@
# Changelog
## [2.10.0] - 2025-08-04
### Features / Changes
- Also lookup key contacts in lookup_id_by_addr() ([#7073](https://github.com/chatmail/core/pull/7073)).
### Miscellaneous Tasks
- cargo: Bump serde_json from 1.0.140 to 1.0.142.
- cargo: Bump bolero from 0.13.3 to 0.13.4.
- cargo: Bump async-channel from 2.3.1 to 2.5.0.
- cargo: Bump hyper-util from 0.1.14 to 0.1.16.
- cargo: Bump criterion from 0.6.0 to 0.7.0.
- cargo: Bump strum from 0.27.1 to 0.27.2.
- cargo: Bump strum_macros from 0.27.1 to 0.27.2.
- Upgrade async-imap to 0.11.1.
## [2.9.0] - 2025-07-31
### Features / Changes
- repl: Add import-vcard and make-vcard commands ([#7048](https://github.com/chatmail/core/pull/7048)).
### Fixes
- Display correct timer value for ephemeral timer changes.
- Get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker.
### Refactor
- Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts().
- Don't call add_or_lookup_key_contacts() in advance.
## [2.8.0] - 2025-07-28
### Features / Changes
- Remove ProtectionBroken, make such chats Unprotected ([#7041](https://github.com/chatmail/core/pull/7041)).
### Fixes
- Lookup self by address if there is no fingerprint or gossip.
## [2.7.0] - 2025-07-26
### Features / Changes
- Mimefactory: Order message recipients by time of addition ([#6872](https://github.com/chatmail/core/pull/6872)).
- Put the debug/release build version into the info ([#7034](https://github.com/chatmail/core/pull/7034)).
### Fixes
- Realtime late join ([#6869](https://github.com/chatmail/core/pull/6869)).
- Do not fail to upgrade if the verifier of a contact doesn't exist anymore ([#7044](https://github.com/chatmail/core/pull/7044)).
### Tests
- Add regression test for verification-gossiping crash ([#7033](https://github.com/chatmail/core/pull/7033)).
## [2.6.0] - 2025-07-23
### Fixes
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
## [2.5.0] - 2025-07-22
### Fixes
- Correctly migrate "verified by me".
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
- Do not ignore errors in add_flag_finalized_with_set.
### Documentation
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
- STYLE.md: Prefer `try_next()` over `next()`.
## [2.4.0] - 2025-07-21
### Fixes
@@ -6511,3 +6590,9 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
[2.7.0]: https://github.com/chatmail/core/compare/v2.6.0..v2.7.0
[2.8.0]: https://github.com/chatmail/core/compare/v2.7.0..v2.8.0
[2.9.0]: https://github.com/chatmail/core/compare/v2.8.0..v2.9.0
[2.10.0]: https://github.com/chatmail/core/compare/v2.9.0..v2.10.0

136
Cargo.lock generated
View File

@@ -229,9 +229,9 @@ dependencies = [
[[package]]
name = "async-channel"
version = "2.3.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
@@ -268,11 +268,11 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.11.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9f9a9c94a403cf46aa2b4cecbceefc6e4284441ebbeca79b80f3bab4394458"
checksum = "8da885da5980f3934831e6370445c0e0e44ef251d7792308b39e908915a41d09"
dependencies = [
"async-channel 2.3.1",
"async-channel 2.5.0",
"async-compression",
"base64",
"bytes",
@@ -535,9 +535,9 @@ dependencies = [
[[package]]
name = "bolero"
version = "0.13.3"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e913ed74716cd68dc5be41c702327b1cc4ffc8f0b55945ae46fb015777007eb"
checksum = "0ff44d278fc0062c95327087ed96b3d256906d1d8f579e534a3de8d6b386913a"
dependencies = [
"bolero-afl",
"bolero-engine",
@@ -561,9 +561,9 @@ dependencies = [
[[package]]
name = "bolero-engine"
version = "0.13.3"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05cae8c41807b046bb7005f52fa60c8f67787c1bf272242f0b84224853e04ceb"
checksum = "dca199170a7c92c669c1019f9219a316b66bcdcfa4b36cac5a460a4c1a851aba"
dependencies = [
"anyhow",
"bolero-generator",
@@ -575,9 +575,9 @@ dependencies = [
[[package]]
name = "bolero-generator"
version = "0.13.4"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3ac7405f187921256faa515fa05ae02521103582a9d938410cefabe3a9a172"
checksum = "98a5782f2650f80d533f58ec339c6dce4cc5428f9c2755894f98156f52af81f2"
dependencies = [
"bolero-generator-derive",
"either",
@@ -588,14 +588,14 @@ dependencies = [
[[package]]
name = "bolero-generator-derive"
version = "0.13.3"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c56c2f8c1c0707d678bebb36168cfd523c45927bb8d9cb7567d3578fa428cbd"
checksum = "9a21a3b022507b9edd2050caf370d945e398c1a7c8455531220fa3968c45d29e"
dependencies = [
"proc-macro-crate 2.0.0",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.104",
]
[[package]]
@@ -1023,16 +1023,16 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"itertools 0.13.0",
"itertools",
"num-traits",
"oorandom",
"plotters",
@@ -1047,12 +1047,12 @@ dependencies = [
[[package]]
name = "criterion-plot"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
dependencies = [
"cast",
"itertools 0.10.5",
"itertools",
]
[[package]]
@@ -1285,11 +1285,11 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.4.0"
version = "2.10.0"
dependencies = [
"anyhow",
"async-broadcast",
"async-channel 2.3.1",
"async-channel 2.5.0",
"async-imap",
"async-native-tls",
"async-smtp",
@@ -1353,8 +1353,8 @@ dependencies = [
"sha2",
"shadowsocks",
"smallvec",
"strum 0.27.1",
"strum_macros 0.27.1",
"strum 0.27.2",
"strum_macros 0.27.2",
"tagger",
"tempfile",
"testdir",
@@ -1395,10 +1395,10 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.4.0"
version = "2.10.0"
dependencies = [
"anyhow",
"async-channel 2.3.1",
"async-channel 2.5.0",
"base64",
"deltachat",
"deltachat-contact-tools",
@@ -1417,7 +1417,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.4.0"
version = "2.10.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1433,7 +1433,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.4.0"
version = "2.10.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1462,7 +1462,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.4.0"
version = "2.10.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1923,9 +1923,9 @@ dependencies = [
[[package]]
name = "event-listener-strategy"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener 5.4.0",
"pin-project-lite",
@@ -2557,9 +2557,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "human-panic"
version = "2.0.2"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80b84a66a325082740043a6c28bbea400c129eac0d3a27673a1de971e44bf1f7"
checksum = "ac63a746b187e95d51fe16850eb04d1cfef203f6af98e6c405a6f262ad3df00a"
dependencies = [
"backtrace",
"os_info",
@@ -2619,9 +2619,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.14"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
dependencies = [
"bytes",
"futures-channel",
@@ -3019,7 +3019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ca43045ceb44b913369f417d56323fb1628ebf482ab4c1e9360e81f1b58cbc2"
dependencies = [
"anyhow",
"async-channel 2.3.1",
"async-channel 2.5.0",
"bytes",
"derive_more 1.0.0",
"ed25519-dalek",
@@ -3169,15 +3169,6 @@ dependencies = [
"z32",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
@@ -5393,9 +5384,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [
"itoa",
"memchr",
@@ -5405,9 +5396,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.9"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
"serde",
]
@@ -5726,9 +5717,9 @@ dependencies = [
[[package]]
name = "strum"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
[[package]]
name = "strum_macros"
@@ -5745,14 +5736,13 @@ dependencies = [
[[package]]
name = "strum_macros"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.104",
]
@@ -6186,14 +6176,17 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.23"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.27",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
"winnow 0.7.11",
]
[[package]]
@@ -6201,6 +6194,12 @@ name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde",
]
@@ -6212,7 +6211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [
"indexmap",
"toml_datetime",
"toml_datetime 0.6.11",
"winnow 0.5.40",
]
@@ -6223,19 +6222,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.11",
]
[[package]]
name = "toml_parser"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
dependencies = [
"winnow 0.7.11",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]]
name = "tower"
version = "0.5.2"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.4.0"
version = "2.10.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
@@ -44,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.11.0", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
@@ -63,7 +63,7 @@ hickory-resolver = "0.25.2"
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"
hyper-util = "0.1.14"
hyper-util = "0.1.16"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
@@ -107,7 +107,7 @@ tokio-stream = { version = "0.1.17", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.8"
toml = "0.9"
tracing = "0.1.41"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
@@ -116,7 +116,7 @@ blake3 = "1.8.2"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.6.0", features = ["async_tokio"] }
criterion = { version = "0.7.0", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -175,7 +175,7 @@ harness = false
[workspace.dependencies]
anyhow = "1"
async-channel = "2.3.1"
async-channel = "2.5.0"
base64 = "0.22"
chrono = { version = "0.4.41", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }

View File

@@ -80,18 +80,26 @@ Connect to your mail server (if already configured):
> connect
```
Create a contact:
Export your public key to a vCard file:
```
> make-vcard my.vcard 1
```
Create contacts by address or vCard file:
```
> addcontact yourfriends@email.org
> import-vcard key-contact.vcard
```
List contacts:
```
> listcontacts
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
Contact#Contact#Self: Me √ <your@email.org>
1 key contacts.
2 key contacts.
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
1 address contacts.
```

View File

@@ -78,6 +78,27 @@ All errors should be handled in one of these ways:
- With `.log_err().ok()`.
- Bubbled up with `?`.
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
```
while let Some(event) = stream.try_next().await? {
todo!();
}
```
instead of
```
while let Some(event_res) = stream.next().await {
todo!();
}
```
as it allows bubbling up the error early with `?`
with no way to accidentally skip error processing
with early `continue` or `break`.
Some streams reading from a connection
return infinite number of `Some(Err(_))`
items when connection breaks and not processing
errors may result in infinite loop.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`

View File

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

View File

@@ -503,13 +503,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `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.
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
* to 1 if it supports verified 1:1 chats.
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
* and when the key changes, an info message is posted into the chat.
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `is_muted` = Whether a context is muted by the user.
* Muted contexts should not sound, vibrate or show notifications.
@@ -1339,12 +1332,14 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
* Optionally, some special markers added to the ID array may help to
* implement virtual lists.
*
* To get the concrete time of the message, use dc_array_get_timestamp().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID of which the messages IDs should be queried.
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
* To get the concrete time of the marker, use dc_array_get_timestamp().
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
* @param marker1before Deprecated, set this to 0.
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
@@ -2094,9 +2089,19 @@ int dc_may_be_valid_addr (const char* addr);
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* Looks up a known and unblocked contact with a given e-mail address.
* To get a list of all known and unblocked contacts, use dc_get_contacts().
*
* **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
* (e.g. an address-contact and a key-contact),
* this looks up the most recently seen contact,
* i.e. which contact is returned depends on which contact last sent a message.
* If the user just clicked on a mailto: link, then this is the best thing you can do.
* But **DO NOT** internally represent contacts by their email address
* and do not use this function to look them up;
* otherwise this function will sometimes look up the wrong contact.
* Instead, you should internally represent contacts by their ids.
*
* To validate an e-mail address independently of the contact database
* use dc_may_be_valid_addr().
*
@@ -2118,6 +2123,13 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
* a bunch of addresses.
*
* This will always create or look up an address-contact,
* i.e. a contact identified by an email address,
* with all messages sent to and from this contact being unencrypted.
* If the user just clicked on an email address,
* you should first check `lookup_contact_id_by_addr`,
* and only if there is no contact yet, call this function here.
*
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
*
* @memberof dc_context_t
@@ -3818,21 +3830,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
/**
* Check if a chat is protected.
*
* End-to-end encryption is guaranteed in protected chats
* and only verified contacts
* Only verified contacts
* as determined by dc_contact_is_verified()
* can be added to protected chats.
*
* Protected chats are created using dc_create_group_chat()
* by setting the 'protect' parameter to 1.
* 1:1 chats become protected or unprotected automatically
* if `verified_one_on_one_chats` setting is enabled.
*
* UI should display a green checkmark
* in the chat title,
* in the chatlist item
* and in the chat profile
* if chat protection is enabled.
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3869,6 +3872,8 @@ int dc_chat_is_encrypted (const dc_chat_t *chat);
*
* The UI should let the user confirm that this is OK with a message like
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
*
* @deprecated 2025-07 chats protection cannot break any longer
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
@@ -5267,20 +5272,14 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if the contact
* can be added to verified chats,
* i.e. has a verified key
* and Autocrypt key matches the verified key.
* can be added to protected chats.
*
* If contact is verified
* UI should display green checkmark after the contact name
* in contact list items,
* in chat member list items
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
* See dc_contact_get_verifier_id() for a guidance how to display these information.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
* 2: SELF and contact have verified their fingerprints in both directions.
*/
int dc_contact_is_verified (dc_contact_t* contact);
@@ -5311,16 +5310,22 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
*
* If the function returns non-zero result,
* display green checkmark in the profile and "Introduced by ..." line
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr.
* As verifier may be unknown,
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
*
* If this function returns a verifier,
* this does not necessarily mean
* you can add the contact to verified chats.
* Use dc_contact_is_verified() to check
* if a contact can be added to a verified chat instead.
* UI should display the information in the contact's profile as follows:
*
* - If dc_contact_get_verifier_id() != 0,
* display text "Introduced by ..."
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr().
* Prefix the text by a green checkmark.
*
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
* display "Introduced" prefixed by a green checkmark.
*
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
* display nothing
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -6386,7 +6391,6 @@ void dc_event_unref(dc_event_t* event);
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
* and dc_remove_contact_from_chat().
*

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.4.0"
version = "2.10.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -1227,8 +1227,10 @@ impl CommandApi {
}
/// Returns all messages of a particular chat.
/// If `add_daymarker` is `true`, it will return them as
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
///
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
/// corresponding (following) day in the local timezone.
async fn get_message_ids(
&self,
account_id: u32,
@@ -1471,7 +1473,14 @@ impl CommandApi {
/// Add a single contact as a result of an explicit user action.
///
/// Returns contact id of the created or existing contact
/// This will always create or look up an address-contact,
/// i.e. a contact identified by an email address,
/// with all messages sent to and from this contact being unencrypted.
/// If the user just clicked on an email address,
/// you should first check [`Self::lookup_contact_id_by_addr`]/`lookupContactIdByAddr.`,
/// and only if there is no contact yet, call this function here.
///
/// Returns contact id of the created or existing contact.
async fn create_contact(
&self,
account_id: u32,
@@ -1621,9 +1630,19 @@ impl CommandApi {
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
}
/// Check if an e-mail address belongs to a known and unblocked contact.
/// Looks up a known and unblocked contact with a given e-mail address.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
///
/// To validate an e-mail address independently of the contact database
/// use check_email_validity().
async fn lookup_contact_id_by_addr(

View File

@@ -21,15 +21,16 @@ pub struct FullChat {
/// True if the chat is protected.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
/// Only verified contacts
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
/// can be added to protected chats.
///
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
/// by setting the 'protect' parameter to true.
///
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
@@ -70,7 +71,9 @@ pub struct FullChat {
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
/// Deprecated 2025-07. Chats protection cannot break any longer.
is_protection_broken: bool,
is_device_chat: bool,
self_in_group: bool,
is_muted: bool,
@@ -215,7 +218,9 @@ pub struct BasicChat {
is_self_talk: bool,
color: String,
is_contact_request: bool,
/// Deprecated 2025-07. Chats protection cannot break any longer.
is_protection_broken: bool,
is_device_chat: bool,
is_muted: bool,
}

View File

@@ -31,13 +31,11 @@ pub struct ContactObject {
/// e.g. if we just scanned the fingerprint from a QR code.
e2ee_avail: bool,
/// True if the contact can be added to verified groups.
/// True if the contact
/// can be added to protected chats
/// because SELF and contact have verified their fingerprints in both directions.
///
/// If this is true
/// UI should display green checkmark after the contact name
/// in contact list items,
/// in chat member list items
/// and in profiles if no chat with the contact exist.
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
is_verified: bool,
/// True if the contact profile title should have a green checkmark.
@@ -46,12 +44,29 @@ pub struct ContactObject {
/// or will have a green checkmark if created.
is_profile_verified: bool,
/// The ID of the contact that verified this contact.
/// The contact ID that verified a contact.
///
/// If this is present,
/// display a green checkmark and "Introduced by ..."
/// string followed by the verifier contact name and address
/// in the contact profile.
/// As verifier may be unknown,
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
///
/// UI should display the information in the contact's profile as follows:
///
/// - If `verifierId` != 0,
/// display text "Introduced by ..."
/// with the name and address of the contact
/// formatted by `name_and_addr`/`nameAndAddr`.
/// Prefix the text by a green checkmark.
///
/// - If `verifierId` == 0 and `isVerified` != 0,
/// display "Introduced" prefixed by a green checkmark.
///
/// - if `verifierId` == 0 and `isVerified` == 0,
/// display nothing
///
/// This contains the contact ID of the verifier.
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
/// If it is None/Null, we don't have verifier information or
/// the contact is not verified.
verifier_id: Option<u32>,
/// the contact's last seen timestamp

View File

@@ -224,7 +224,6 @@ pub enum EventType {
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See setChatName(), setChatProfileImage(), addContactToChat()
/// and removeContactFromChat().
///

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.4.0"
"version": "2.10.0"
}

View File

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

View File

@@ -403,6 +403,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
block <contact-id>\n\
unblock <contact-id>\n\
listblocked\n\
import-vcard <file>\n\
make-vcard <file> <contact-id> [contact-id ...]\n\
======================================Misc.==\n\
getqr [<chat-id>]\n\
getqrsvg [<chat-id>]\n\
@@ -1218,6 +1220,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
log_contactlist(&context, &contacts).await?;
println!("{} blocked contacts.", contacts.len());
}
"import-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
let contacts = import_vcard(&context, &vcard_content).await?;
println!("vCard contacts imported:");
log_contactlist(&context, &contacts).await?;
}
"make-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
let mut contact_ids = vec![];
for x in arg2.split_whitespace() {
contact_ids.push(ContactId::new(x.parse()?))
}
let vcard_content = make_vcard(&context, &contact_ids).await?;
fs::write(&arg1.to_string(), vcard_content).await?;
println!("vCard written to: {arg1}");
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let qr = check_qr(&context, arg1).await?;

View File

@@ -232,7 +232,7 @@ const MESSAGE_COMMANDS: [&str; 10] = [
"delmsg",
"react",
];
const CONTACT_COMMANDS: [&str; 7] = [
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
"addcontact",
"contactinfo",
@@ -240,6 +240,8 @@ const CONTACT_COMMANDS: [&str; 7] = [
"block",
"unblock",
"listblocked",
"import-vcard",
"make-vcard",
];
const MISC_COMMANDS: [&str; 14] = [
"getqr",

View File

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

View File

@@ -185,7 +185,21 @@ class Account:
return Contact(self, contact_id)
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Check if an e-mail address belongs to a known and unblocked contact."""
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)

View File

@@ -171,7 +171,10 @@ def test_account(acfactory) -> None:
assert alice.get_size()
assert alice.is_configured()
assert not alice.get_avatar()
assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
# get_contact_by_addr() can lookup a key contact by address:
bob_contact = alice.get_contact_by_addr(bob_addr).get_snapshot()
assert bob_contact.display_name == "Bob"
assert bob_contact.is_key_contact
assert alice.get_contacts()
assert alice.get_contacts(snapshot=True)
assert alice.self_contact

View File

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

View File

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

View File

@@ -48,6 +48,7 @@ skip = [
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },

View File

@@ -6,7 +6,7 @@ edition = "2021"
license = "MPL-2.0"
[dev-dependencies]
bolero = "0.13.3"
bolero = "0.13.4"
[dependencies]
mailparse = { workspace = true }

View File

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

View File

@@ -330,7 +330,21 @@ class Account:
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
"""get a contact for the email address or None if it's blocked or doesn't exist."""
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)

View File

@@ -1 +1 @@
2025-07-21
2025-08-04

View File

@@ -94,14 +94,12 @@ pub enum ProtectionStatus {
///
/// All members of the chat must be verified.
Protected = 1,
// `2` was never used as a value.
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
/// The user has to confirm that this is OK.
///
/// We only do this in 1:1 chats; in group chats, the chat just
/// stays protected.
ProtectionBroken = 3, // `2` was never used as a value.
// Chats don't break in Core v2 anymore. Chats with broken protection existing before the
// key-contacts migration are treated as `Unprotected`.
//
// ProtectionBroken = 3,
}
/// The reason why messages cannot be sent to the chat.
@@ -118,10 +116,6 @@ pub(crate) enum CantSendReason {
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
ProtectionBroken,
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
@@ -144,10 +138,6 @@ impl fmt::Display for CantSendReason {
f,
"contact request chat should be accepted before sending messages"
),
Self::ProtectionBroken => write!(
f,
"accept that the encryption isn't verified anymore before sending messages"
),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
@@ -479,16 +469,6 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Single
if chat.blocked == Blocked::Not
&& chat.protected == ProtectionStatus::ProtectionBroken =>
{
// The protection was broken, then the user clicked 'Accept'/'OK',
// so, now we want to set the status to Unprotected again:
chat.id
.inner_set_protection(context, ProtectionStatus::Unprotected)
.await?;
}
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
// User has "created a chat" with all these contacts.
//
@@ -545,7 +525,7 @@ impl ChatId {
| Chattype::InBroadcast => {}
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
},
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
ProtectionStatus::Unprotected => {}
};
context
@@ -588,7 +568,6 @@ impl ChatId {
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
add_info_msg_with_cmd(
context,
@@ -1700,12 +1679,6 @@ impl Chat {
return Ok(Some(reason));
}
}
if self.is_protection_broken() {
let reason = ProtectionBroken;
if !skip_fn(&reason) {
return Ok(Some(reason));
}
}
if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
let reason = ReadOnlyMailingList;
if !skip_fn(&reason) {
@@ -1935,25 +1908,9 @@ impl Chat {
Ok(is_encrypted)
}
/// Returns true if the chat was protected, and then an incoming message broke this protection.
///
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
/// otherwise it will return false for all chats.
///
/// 1:1 chats are automatically set as protected when a contact is verified.
/// When a message comes in that is not encrypted / signed correctly,
/// the chat is automatically set as unprotected again.
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
///
/// The UI should let the user confirm that this is OK with a message like
/// `Bob sent a message from another device. Tap to learn more`
/// and then call `chat_id.accept()`.
/// Deprecated 2025-07. Returns false.
pub fn is_protection_broken(&self) -> bool {
match self.protected {
ProtectionStatus::Protected => false,
ProtectionStatus::Unprotected => false,
ProtectionStatus::ProtectionBroken => true,
}
false
}
/// Returns true if location streaming is enabled in the chat.
@@ -2947,7 +2904,7 @@ async fn prepare_send_msg(
let mut chat = Chat::load_from_db(context, chat_id).await?;
let skip_fn = |reason: &CantSendReason| match reason {
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
CantSendReason::ContactRequest => {
// Allow securejoin messages, they are supposed to repair the verification.
// If the chat is a contact request, let the user accept it later.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
@@ -3366,10 +3323,11 @@ pub async fn get_chat_msgs_ex(
for (ts, curr_id) in sorted_rows {
if add_daymarker {
let curr_local_timestamp = ts + cnv_to_local;
let curr_day = curr_local_timestamp / 86400;
let secs_in_day = 86400;
let curr_day = curr_local_timestamp / secs_in_day;
if curr_day != last_day {
ret.push(ChatItem::DayMarker {
timestamp: curr_day * 86400, // Convert day back to Unix timestamp
timestamp: curr_day * secs_in_day - cnv_to_local,
});
last_day = curr_day;
}

View File

@@ -245,9 +245,6 @@ impl Chatlist {
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
// time. It may be confusing if a chat that is normally in the list disappears
// suddenly. The UI need to deal with that case anyway.
context.sql.query_map(
"SELECT c.id, c.type, c.param, m.id
FROM chats c

View File

@@ -417,7 +417,7 @@ pub enum Config {
#[strum(props(default = "172800"))]
GossipPeriod,
/// Feature flag for verified 1:1 chats; the UI should set it
/// Deprecated 2025-07. Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
/// and when the key changes, an info message is posted into the chat.

View File

@@ -755,7 +755,19 @@ impl Contact {
self.is_bot
}
/// Check if an e-mail address belongs to a known and unblocked contact.
/// Looks up a known and unblocked contact with a given e-mail address.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
///
/// Known and unblocked contacts will be returned by `get_contacts()`.
///
@@ -795,8 +807,8 @@ impl Contact {
.query_get_value(
"SELECT id FROM contacts
WHERE addr=?1 COLLATE NOCASE
AND fingerprint='' -- Do not lookup key-contacts
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
AND id>?2 AND origin>=?3 AND (? OR blocked=?)
ORDER BY last_seen DESC LIMIT 1",
(
&addr_normalized,
ContactId::LAST_SPECIAL,

View File

@@ -1072,6 +1072,7 @@ async fn test_sync_create() -> Result<()> {
.unwrap();
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.name, "Bob");
assert_eq!(a1b_contact.is_key_contact(), false);
Contact::create(alice0, "Bob Renamed", "bob@example.net").await?;
test_utils::sync(alice0, alice1).await;
@@ -1081,6 +1082,7 @@ async fn test_sync_create() -> Result<()> {
assert_eq!(id, a1b_contact_id);
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.name, "Bob Renamed");
assert_eq!(a1b_contact.is_key_contact(), false);
Ok(())
}

View File

@@ -333,6 +333,15 @@ impl Default for RunningState {
/// about the context on top of the information here.
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();
#[cfg(debug_assertions)]
res.insert(
"debug_assertions",
"On - DO NOT RELEASE THIS BUILD".to_string(),
);
#[cfg(not(debug_assertions))]
res.insert("debug_assertions", "Off".to_string());
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
@@ -1043,7 +1052,7 @@ impl Context {
self.get_config_int(Config::GossipPeriod).await?.to_string(),
);
res.insert(
"verified_one_on_one_chats",
"verified_one_on_one_chats", // deprecated 2025-07
self.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
.to_string(),
@@ -1078,7 +1087,6 @@ impl Context {
#[derive(Default)]
struct ChatNumbers {
protected: u32,
protection_broken: u32,
opportunistic_dc: u32,
opportunistic_mua: u32,
unencrypted_dc: u32,
@@ -1114,7 +1122,6 @@ impl Context {
// how many of the chats active in the last months are:
// - protected
// - protection-broken
// - opportunistic-encrypted and the contact uses Delta Chat
// - opportunistic-encrypted and the contact uses a classical MUA
// - unencrypted and the contact uses Delta Chat
@@ -1157,8 +1164,6 @@ impl Context {
if protected == ProtectionStatus::Protected {
chats.protected += 1;
} else if protected == ProtectionStatus::ProtectionBroken {
chats.protection_broken += 1;
} else if encrypted {
if is_dc_message {
chats.opportunistic_dc += 1;
@@ -1176,7 +1181,6 @@ impl Context {
)
.await?;
res += &format!("chats_protected {}\n", chats.protected);
res += &format!("chats_protection_broken {}\n", chats.protection_broken);
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);

View File

@@ -128,31 +128,33 @@ async fn test_stock_ephemeral_messages() {
/// Test enabling and disabling ephemeral timer remotely.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ephemeral_enable_disable() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat_alice = alice.create_chat(&bob).await.id;
let chat_bob = bob.create_chat(&alice).await.id;
let chat_alice = alice.create_chat(bob).await.id;
let chat_bob = bob.create_chat(alice).await.id;
chat_alice
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
.await?;
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
let bob_received_message = bob.recv_msg(&sent).await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
bob_received_message.text,
"Message deletion timer is set to 1 minute by alice@example.org."
);
assert_eq!(
chat_bob.get_ephemeral_timer(bob).await?,
Timer::Enabled { duration: 60 }
);
chat_alice
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
.set_ephemeral_timer(alice, Timer::Disabled)
.await?;
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Disabled
);
assert_eq!(chat_bob.get_ephemeral_timer(bob).await?, Timer::Disabled);
Ok(())
}

View File

@@ -17,7 +17,7 @@ use anyhow::{Context as _, Result, bail, ensure, format_err};
use async_channel::{self, Receiver, Sender};
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
use deltachat_contact_tools::ContactAddress;
use futures::{FutureExt as _, StreamExt, TryStreamExt};
use futures::{FutureExt as _, TryStreamExt};
use futures_lite::FutureExt;
use num_traits::FromPrimitive;
use rand::Rng;
@@ -1698,7 +1698,7 @@ impl Session {
.uid_store(uid_set, &query)
.await
.with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
while let Some(_response) = responses.next().await {
while let Some(_response) = responses.try_next().await? {
// Read all the responses
}
Ok(())

View File

@@ -804,7 +804,7 @@ async fn export_database(
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
[],
)
.ok(); // If verified_one_on_one_chats was not set, this errors, which we ignore
.ok(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database")?;
res?;

View File

@@ -5,7 +5,9 @@ use std::io::Cursor;
use anyhow::{Context as _, Result, bail, ensure};
use base64::Engine as _;
use data_encoding::BASE32_NOPAD;
use deltachat_contact_tools::sanitize_bidi_characters;
use iroh_gossip::proto::TopicId;
use mail_builder::headers::HeaderType;
use mail_builder::headers::address::{Address, EmailAddress};
use mail_builder::mime::MimePart;
@@ -22,14 +24,14 @@ use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::ensure_and_debug_assert;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::key::self_fingerprint;
use crate::key::{DcKey, SignedPublicKey};
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, SignedPublicKey, self_fingerprint};
use crate::location;
use crate::log::{info, warn};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{SystemMessage, is_hidden};
use crate::param::Param;
use crate::peer_channels::create_iroh_header;
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
use crate::simplify::escape_message_footer_marks;
use crate::stock_str;
use crate::tools::{
@@ -140,6 +142,9 @@ pub struct MimeFactory {
/// True if the avatar should be attached.
pub attach_selfavatar: bool,
/// This field is used to sustain the topic id of webxdcs needed for peer channels.
webxdc_topic: Option<TopicId>,
}
/// Result of rendering a message, ready to be submitted to a send job.
@@ -248,6 +253,10 @@ impl MimeFactory {
let mut missing_key_addresses = BTreeSet::new();
context
.sql
// Sort recipients by `add_timestamp DESC` so that if the group is large and there
// are multiple SMTP messages, a newly added member receives the member addition
// message earlier and has gossiped keys of other members (otherwise the new member
// may receive messages from other members earlier and fail to verify them).
.query_map(
"SELECT
c.authname,
@@ -261,7 +270,8 @@ impl MimeFactory {
LEFT JOIN contacts c ON cc.contact_id=c.id
LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint
WHERE cc.chat_id=?
AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))",
AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))
ORDER BY cc.add_timestamp DESC",
(msg.chat_id, chat.typ == Chattype::Group),
|row| {
let authname: String = row.get(0)?;
@@ -460,7 +470,7 @@ impl MimeFactory {
past_members.len(),
member_timestamps.len(),
);
let webxdc_topic = get_iroh_topic_for_msg(context, msg.id).await?;
let factory = MimeFactory {
from_addr,
from_displayname,
@@ -480,6 +490,7 @@ impl MimeFactory {
last_added_location_id: None,
sync_ids_to_delete: None,
attach_selfavatar,
webxdc_topic,
};
Ok(factory)
}
@@ -527,6 +538,7 @@ impl MimeFactory {
last_added_location_id: None,
sync_ids_to_delete: None,
attach_selfavatar: false,
webxdc_topic: None,
};
Ok(res)
@@ -1510,7 +1522,7 @@ impl MimeFactory {
}
SystemMessage::IrohNodeAddr => {
headers.push((
"Iroh-Node-Addr",
HeaderDef::IrohNodeAddr.into(),
mail_builder::headers::text::Text::new(serde_json::to_string(
&context
.get_or_try_init_peer_channel()
@@ -1691,10 +1703,13 @@ 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 = self
.webxdc_topic
.map(|top| BASE32_NOPAD.encode(top.as_bytes()).to_ascii_lowercase())
.unwrap_or(create_iroh_header(context, msg.id).await?);
headers.push((
"Iroh-Gossip-Topic",
mail_builder::headers::raw::Raw::new(create_iroh_header(context, msg.id).await?)
.into(),
HeaderDef::IrohGossipTopic.get_headername(),
mail_builder::headers::raw::Raw::new(topic).into(),
));
if let (Some(json), _) = context
.render_webxdc_status_update_object(

View File

@@ -2,6 +2,7 @@ use deltachat_contact_tools::ContactAddress;
use mail_builder::headers::Header;
use mailparse::{MailHeaderMap, addrparse_header};
use std::str;
use std::time::Duration;
use super::*;
use crate::chat::{
@@ -16,6 +17,7 @@ use crate::message;
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
use crate::tools::SystemTime;
fn render_email_address(display_name: &str, addr: &str) -> String {
let mut output = Vec::<u8>::new();
@@ -681,6 +683,7 @@ async fn test_selfavatar_unencrypted_signed() {
.unwrap()
.unwrap();
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert_eq!(alice_contact.is_key_contact(), false);
assert!(
alice_contact
.get_profile_image(&bob.ctx)
@@ -867,6 +870,43 @@ async fn test_dont_remove_self() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_new_member_is_first_recipient() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
let group = alice
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
.await;
alice.send_text(group, "Hi! I created a group.").await;
SystemTime::shift(Duration::from_secs(60));
add_contact_to_chat(alice, group, charlie_id).await?;
let sent_msg = alice.pop_sent_msg().await;
assert!(
sent_msg
.recipients
.starts_with(&charlie.get_config(Config::Addr).await?.unwrap())
);
remove_contact_from_chat(alice, group, bob_id).await?;
alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_secs(60));
add_contact_to_chat(alice, group, bob_id).await?;
let sent_msg = alice.pop_sent_msg().await;
assert!(
sent_msg
.recipients
.starts_with(&bob.get_config(Config::Addr).await?.unwrap())
);
Ok(())
}
/// Regression test: mimefactory should never create an empty to header,
/// also not if the Selftalk parameter is missing
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -48,13 +48,13 @@ use crate::mimeparser::SystemMessage;
const PUBLIC_KEY_LENGTH: usize = 32;
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
/// Store iroh peer channels for the context.
/// Store Iroh peer channels for the context.
#[derive(Debug)]
pub struct Iroh {
/// iroh router needed for iroh peer channels.
/// Iroh router needed for Iroh peer channels.
pub(crate) router: iroh::protocol::Router,
/// [Gossip] needed for iroh peer channels.
/// [Gossip] needed for Iroh peer channels.
pub(crate) gossip: Gossip,
/// Sequence numbers for gossip channels.
@@ -109,7 +109,7 @@ impl Iroh {
info!(
ctx,
"IROH_REALTIME: Joining gossip with peers: {:?}", node_ids,
"IROH_REALTIME: Joining gossip {topic} with peers: {:?}.", node_ids,
);
// Inform iroh of potentially new node addresses
@@ -138,17 +138,11 @@ impl Iroh {
Ok(Some(join_rx))
}
/// Add gossip peers to realtime channel if it is already active.
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
/// Add gossip peer to realtime channel if it is already active.
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
if self.iroh_channels.read().await.get(&topic).is_some() {
for peer in &peers {
self.router.endpoint().add_node_addr(peer.clone())?;
}
self.gossip.subscribe_with_opts(
topic,
JoinOptions::with_bootstrap(peers.into_iter().map(|peer| peer.node_id)),
);
self.router.endpoint().add_node_addr(peer.clone())?;
self.gossip.subscribe(topic, vec![peer.node_id])?;
}
Ok(())
}
@@ -316,6 +310,17 @@ impl Context {
}
}
}
pub(crate) async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
if let Some(iroh) = &*self.iroh.read().await {
info!(
self,
"Adding (maybe existing) peer with id {} to {topic}.", peer.node_id
);
iroh.maybe_add_gossip_peer(topic, peer).await?;
}
Ok(())
}
}
/// Cache a peers [NodeId] for one topic.
@@ -348,13 +353,14 @@ pub async fn add_gossip_peer_from_header(
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")?;
info!(
context,
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.node_id
);
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
msg_id: instance_id,
});
@@ -371,8 +377,7 @@ pub async fn add_gossip_peer_from_header(
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?;
context.maybe_add_gossip_peer(topic, node_addr).await?;
Ok(())
}
@@ -555,9 +560,9 @@ mod tests {
use super::*;
use crate::{
EventType,
chat::send_msg,
chat::{self, ChatId, ProtectionStatus, add_contact_to_chat, resend_msgs, send_msg},
message::{Message, Viewtype},
test_utils::TestContextManager,
test_utils::{TestContext, TestContextManager},
};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -924,8 +929,8 @@ mod tests {
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
// Alice sends webxdc to bob
let alice_chat = alice.create_chat(bob).await;
let chat = alice.create_chat(bob).await.id;
let mut instance = Message::new(Viewtype::File);
instance
.set_file_from_bytes(
@@ -935,7 +940,100 @@ mod tests {
None,
)
.unwrap();
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
connect_alice_bob(alice, chat, &mut instance, bob).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_resend() {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
let group = chat::create_group_chat(alice, ProtectionStatus::Unprotected, "group chat")
.await
.unwrap();
// Alice sends webxdc to bob
let mut instance = Message::new(Viewtype::File);
instance
.set_file_from_bytes(
alice,
"minimal.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc"),
None,
)
.unwrap();
add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(bob).await)
.await
.unwrap();
connect_alice_bob(alice, group, &mut instance, bob).await;
// fiona joins late
let fiona = &mut tcm.fiona().await;
add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(fiona).await)
.await
.unwrap();
resend_msgs(alice, &[instance.id]).await.unwrap();
let msg = alice.pop_sent_msg().await;
let fiona_instance = fiona.recv_msg(&msg).await;
fiona_instance.chat_id.accept(fiona).await.unwrap();
assert!(fiona.ctx.iroh.read().await.is_none());
let fiona_connect_future = send_webxdc_realtime_advertisement(fiona, fiona_instance.id)
.await
.unwrap()
.unwrap();
let fiona_advert = fiona.pop_sent_msg().await;
alice.recv_msg_trash(&fiona_advert).await;
fiona_connect_future.await.unwrap();
let realtime_send_loop = async {
// Keep sending in a loop because right after joining
// Fiona may miss messages.
loop {
send_webxdc_realtime_data(alice, instance.id, b"alice -> bob & fiona".into())
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
};
let realtime_receive_loop = async {
loop {
let event = fiona.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
if data == b"alice -> bob & fiona" {
break;
} else {
panic!(
"Unexpected status update: {}",
String::from_utf8_lossy(&data)
);
}
}
}
};
tokio::select!(
_ = realtime_send_loop => {
panic!("Send loop should never finish");
},
_ = realtime_receive_loop => {
return;
}
);
}
async fn connect_alice_bob(
alice: &mut TestContext,
alice_chat_id: ChatId,
instance: &mut Message,
bob: &mut TestContext,
) {
send_msg(alice, alice_chat_id, instance).await.unwrap();
let alice_webxdc = alice.get_last_msg().await;
let webxdc = alice.pop_sent_msg().await;
@@ -952,8 +1050,9 @@ mod tests {
.unwrap();
let alice_advertisement = alice.pop_sent_msg().await;
send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
let bob_advertisement_future = send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
.await
.unwrap()
.unwrap();
let bob_advertisement = bob.pop_sent_msg().await;
@@ -961,8 +1060,9 @@ mod tests {
bob.recv_msg_trash(&alice_advertisement).await;
alice.recv_msg_trash(&bob_advertisement).await;
eprintln!("Alice waits for connection");
eprintln!("Alice and Bob wait for connection");
alice_advertisement_future.await.unwrap();
bob_advertisement_future.await.unwrap();
// Alice sends ephemeral message
eprintln!("Sending ephemeral message");

View File

@@ -297,18 +297,16 @@ async fn get_to_and_past_contact_ids(
past_member_fingerprints = &[];
}
let pgp_to_ids = add_or_lookup_key_contacts_by_address_list(
context,
&mime_parser.recipients,
&mime_parser.gossiped_keys,
to_member_fingerprints,
Origin::Hidden,
)
.await?;
match chat_assignment {
ChatAssignment::GroupChat { .. } => {
to_ids = pgp_to_ids;
to_ids = add_or_lookup_key_contacts(
context,
&mime_parser.recipients,
&mime_parser.gossiped_keys,
to_member_fingerprints,
Origin::Hidden,
)
.await?;
if let Some(chat_id) = chat_id {
past_ids = lookup_key_contacts_by_address_list(
@@ -319,7 +317,7 @@ async fn get_to_and_past_contact_ids(
)
.await?;
} else {
past_ids = add_or_lookup_key_contacts_by_address_list(
past_ids = add_or_lookup_key_contacts(
context,
&mime_parser.past_members,
&mime_parser.gossiped_keys,
@@ -336,7 +334,14 @@ async fn get_to_and_past_contact_ids(
ChatAssignment::ExistingChat { chat_id, .. } => {
let chat = Chat::load_from_db(context, *chat_id).await?;
if chat.is_encrypted(context).await? {
to_ids = pgp_to_ids;
to_ids = add_or_lookup_key_contacts(
context,
&mime_parser.recipients,
&mime_parser.gossiped_keys,
to_member_fingerprints,
Origin::Hidden,
)
.await?;
past_ids = lookup_key_contacts_by_address_list(
context,
&mime_parser.past_members,
@@ -388,6 +393,14 @@ async fn get_to_and_past_contact_ids(
.await?;
}
ChatAssignment::OneOneChat => {
let pgp_to_ids = add_or_lookup_key_contacts(
context,
&mime_parser.recipients,
&mime_parser.gossiped_keys,
to_member_fingerprints,
Origin::Hidden,
)
.await?;
if pgp_to_ids
.first()
.is_some_and(|contact_id| contact_id.is_some())
@@ -429,7 +442,7 @@ async fn get_to_and_past_contact_ids(
context,
&mime_parser.recipients,
to_member_fingerprints,
chat_id,
None,
)
.await?
}
@@ -1461,19 +1474,16 @@ async fn do_chat_assignment(
chat.typ == Chattype::Single,
"Chat {chat_id} is not Single",
);
let mut new_protection = match verified_encryption {
let new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
};
if chat.protected != ProtectionStatus::Unprotected
&& new_protection == ProtectionStatus::Unprotected
// `chat.protected` must be maintained regardless of the `Config::VerifiedOneOnOneChats`.
// That's why the config is checked here, and not above.
&& context.get_config_bool(Config::VerifiedOneOnOneChats).await?
{
new_protection = ProtectionStatus::ProtectionBroken;
}
ensure_and_debug_assert!(
chat.protected == ProtectionStatus::Unprotected
|| new_protection == ProtectionStatus::Protected,
"Chat {chat_id} can't downgrade to Unprotected",
);
if chat.protected != new_protection {
// The message itself will be sorted under the device message since the device
// message is `MessageState::InNoticed`, which means that all following
@@ -1838,6 +1848,8 @@ async fn add_parts(
{
Some(stock_str::msg_location_enabled_by(context, from_id).await)
} else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
// Do not delete the system message itself.
//
// This prevents confusion when timer is changed
@@ -1846,7 +1858,7 @@ async fn add_parts(
// week is left.
ephemeral_timer = EphemeralTimer::Disabled;
Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await)
Some(better_msg)
} else {
None
};
@@ -2149,7 +2161,7 @@ RETURNING id
created_db_entries.push(row_id);
}
// check all parts whether they contain a new logging webxdc
// Maybe set logging xdc and add gossip topics for webxdcs.
for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
// check if any part contains a webxdc topic id
if part.typ == Viewtype::Webxdc {
@@ -3642,7 +3654,7 @@ async fn mark_recipients_as_verified(
return Ok(());
}
for to_id in to_ids.iter().filter_map(|&x| x) {
if to_id == ContactId::SELF {
if to_id == ContactId::SELF || to_id == from_id {
continue;
}
@@ -3730,7 +3742,7 @@ async fn add_or_lookup_contacts_by_address_list(
}
/// Looks up contact IDs from the database given the list of recipients.
async fn add_or_lookup_key_contacts_by_address_list(
async fn add_or_lookup_key_contacts(
context: &Context,
address_list: &[SingleInfo],
gossiped_keys: &HashMap<String, SignedPublicKey>,
@@ -3750,6 +3762,9 @@ async fn add_or_lookup_key_contacts_by_address_list(
fp.hex()
} else if let Some(key) = gossiped_keys.get(addr) {
key.dc_fingerprint().hex()
} else if context.is_self_addr(addr).await? {
contact_ids.push(Some(ContactId::SELF));
continue;
} else {
contact_ids.push(None);
continue;

View File

@@ -991,6 +991,7 @@ async fn test_other_device_writes_to_mailinglist() -> Result<()> {
.await?
.unwrap();
let list_post_contact = Contact::get_by_id(&t, list_post_contact_id).await?;
assert_eq!(list_post_contact.is_key_contact(), false);
assert_eq!(
list_post_contact.param.get(Param::ListId).unwrap(),
"delta.codespeak.net"
@@ -1453,6 +1454,7 @@ async fn test_apply_mailinglist_changes_assigned_by_reply() {
.unwrap()
.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.is_key_contact(), false);
assert_eq!(
contact.param.get(Param::ListId).unwrap(),
"deltachat-core-rust.deltachat.github.com"
@@ -3050,6 +3052,39 @@ async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
Ok(())
}
/// Regression test for a bug where receive_imf() failed
/// if the sender of a verification-gossiping message
/// also put itself into the To header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verification_gossip() -> 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(bob, alice).await;
// This is message sent by Alice with verified encryption
// that gossips Fiona's verification,
// and for some reason, Alice also put herself into the To: header.
let imf_raw =
include_bytes!("../../test-data/message/verification-gossip-also-sent-to-from.eml");
// The regression test is that receive_imf() doesn't panic:
let msg = receive_imf(bob, imf_raw, false).await?.unwrap();
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
assert_eq!(msg.text, "Hello!");
assert!(
bob.add_or_lookup_contact(fiona)
.await
.is_verified(bob)
.await?
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_bot_accepts_another_group_after_qr_scan() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -3649,10 +3684,13 @@ async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> {
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
4
);
let fiona = Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo)
.await?
.unwrap();
assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona).await?);
let fiona_contact_id =
Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo)
.await?
.unwrap();
assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona_contact_id).await?);
let fiona_contact = Contact::get_by_id(&alice, fiona_contact_id).await?;
assert_eq!(fiona_contact.is_key_contact(), false);
Ok(())
}
@@ -5269,3 +5307,48 @@ async fn test_outgoing_unencrypted_chat_assignment() {
let chat = alice.create_email_chat(bob).await;
assert_eq!(received.chat_id, chat.id);
}
/// Tests Bob receiving a message from Alice
/// in a new group she just created
/// with only Alice and Bob.
///
/// The message has no Autocrypt-Gossip
/// headers and no Chat-Group-Member-Fpr header.
/// Such messages were created by core 1.159.5
/// when Alice has bcc_self disabled
/// as Chat-Group-Member-Fpr header did not exist
/// yet and Autocrypt-Gossip is not sent
/// as there is only one recipient
/// (Bob, and no additional Alice devices).
///
/// Bob should recognize self as being
/// a member of the group by just the e-mail address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_group_introduction_no_gossip() -> Result<()> {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let received = receive_imf(
bob,
include_bytes!("../../test-data/message/group-introduction-no-gossip.eml"),
false,
)
.await
.unwrap()
.unwrap();
let msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
.await
.unwrap();
assert_eq!(msg.text, "I created a group");
let chat = Chat::load_from_db(bob, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(chat.blocked, Blocked::Request);
assert_eq!(chat.name, "Group!");
assert!(chat.is_encrypted(bob).await.unwrap());
let contacts = get_chat_contacts(bob, chat.id).await?;
assert_eq!(contacts.len(), 2);
assert!(chat.is_self_in_chat(bob).await?);
Ok(())
}

View File

@@ -414,7 +414,10 @@ pub(crate) async fn send_msg_to_smtp(
.await?;
}
SendResult::Failure(ref err) => {
if err.to_string().contains("Invalid unencrypted mail") {
if err
.to_string()
.to_lowercase()
.contains("invalid unencrypted mail") {
let res = context
.sql
.query_row_optional(

View File

@@ -1251,6 +1251,16 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
.await?;
}
inc_and_check(&mut migration_version, 133)?;
if dbversion < migration_version {
// Make `ProtectionBroken` chats `Unprotected`. Chats can't break anymore.
sql.execute_migration(
"UPDATE chats SET protected=0 WHERE protected!=1",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
@@ -1480,9 +1490,11 @@ fn migrate_key_contacts(
} else if addr.is_empty() {
Ok(default)
} else {
original_contact_id_from_addr_stmt
Ok(original_contact_id_from_addr_stmt
.query_row((addr,), |row| row.get(0))
.with_context(|| format!("Original contact '{addr}' not found"))
.optional()
.with_context(|| format!("Original contact '{addr}' not found"))?
.unwrap_or(default))
}
};
@@ -1505,19 +1517,28 @@ fn migrate_key_contacts(
};
let new_id = insert_contact(verified_key).context("Step 13")?;
verified_key_contacts.insert(original_id.try_into().context("Step 14")?, new_id);
// If the original verifier is unknown, we represent this in the database
// by putting `new_id` into the place of the verifier,
// i.e. we say that this contact verified itself.
let verifier_id =
original_contact_id_from_addr(&verifier, new_id).context("Step 15")?;
let verifier_id = if addr_cmp(&verifier, &addr) {
// Earlier versions of Delta Chat signalled a direct verification
// by putting the contact's own address into the verifier column
1 // 1=ContactId::SELF
} else {
// If the original verifier is unknown, we represent this in the database
// by putting `new_id` into the place of the verifier,
// i.e. we say that this contact verified itself.
original_contact_id_from_addr(&verifier, new_id).context("Step 15")?
};
verifications.insert(new_id, verifier_id);
let Some(secondary_verified_key) = secondary_verified_key else {
continue;
};
let new_id = insert_contact(secondary_verified_key).context("Step 16")?;
let verifier_id: u32 =
original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?;
let verifier_id: u32 = if addr_cmp(&secondary_verifier, &addr) {
1 // 1=ContactId::SELF
} else {
original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?
};
// Only use secondary verification if there is no primary verification:
verifications.entry(new_id).or_insert(verifier_id);
}
@@ -1642,7 +1663,7 @@ fn migrate_key_contacts(
.collect::<Result<Vec<_>, _>>()
.context("Step 26")?;
let mut keep_address_contacts = |reason: &str| {
let mut keep_address_contacts = |reason: &str| -> Result<()> {
info!(
context,
"Chat {chat_id} will be an unencrypted chat with contacts identified by email address: {reason}."
@@ -1650,6 +1671,15 @@ fn migrate_key_contacts(
for (m, _) in &old_members {
orphaned_contacts.remove(m);
}
// Unprotect this chat if it was protected.
//
// Otherwise we get protected chat with address-contact(s).
transaction
.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))
.context("Step 26.0")?;
Ok(())
};
let old_and_new_members: Vec<(u32, bool, Option<u32>)> = match typ {
// 1:1 chats retain:
@@ -1669,19 +1699,13 @@ fn migrate_key_contacts(
};
let Some(new_contact) = map_to_key_contact(old_member) else {
keep_address_contacts("No peerstate, or peerstate in 'reset' state");
keep_address_contacts("No peerstate, or peerstate in 'reset' state")?;
continue;
};
if !addr_cmp_stmt
.query_row((old_member, new_contact), |row| row.get::<_, bool>(0))?
{
// Unprotect this 1:1 chat if it was protected.
//
// Otherwise we get protected chat with address-contact.
transaction
.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?;
keep_address_contacts("key contact has different email");
keep_address_contacts("key contact has different email")?;
continue;
}
vec![(*old_member, true, Some(new_contact))]
@@ -1692,7 +1716,7 @@ fn migrate_key_contacts(
if grpid.is_empty() {
// Ad-hoc group that has empty Chat-Group-ID
// because it was created in response to receiving a non-chat email.
keep_address_contacts("Empty chat-Group-ID");
keep_address_contacts("Empty chat-Group-ID")?;
continue;
} else if protected == 1 {
old_members
@@ -1711,7 +1735,7 @@ fn migrate_key_contacts(
// Mailinglist
140 => {
keep_address_contacts("Mailinglist");
keep_address_contacts("Mailinglist")?;
continue;
}
@@ -1750,7 +1774,7 @@ fn migrate_key_contacts(
transaction
.execute("UPDATE chats SET grpid='' WHERE id=?", (chat_id,))
.context("Step 26.1")?;
keep_address_contacts("Group contains contact without peerstate");
keep_address_contacts("Group contains contact without peerstate")?;
continue;
}

View File

@@ -47,6 +47,7 @@ async fn test_key_contacts_migration_autocrypt() -> Result<()> {
.await?
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
assert_eq!(email_bob.is_key_contact(), false);
assert_eq!(email_bob.origin, Origin::Hidden); // Email bob is in no chats, so, contact is hidden
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
@@ -85,6 +86,7 @@ async fn test_key_contacts_migration_email1() -> Result<()> {
.await?
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
assert_eq!(email_bob.is_key_contact(), false);
assert_eq!(email_bob.origin, Origin::OutgoingTo);
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
@@ -114,6 +116,7 @@ async fn test_key_contacts_migration_email2() -> Result<()> {
.await?
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
assert_eq!(email_bob.is_key_contact(), false);
assert_eq!(email_bob.origin, Origin::OutgoingTo); // Email bob is in no chats, so, contact is hidden
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
@@ -148,6 +151,7 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
dbg!(&email_bob);
assert_eq!(email_bob.is_key_contact(), false);
assert_eq!(email_bob.origin, Origin::Hidden); // Email bob is in no chats, so, contact is hidden
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);

View File

@@ -1292,7 +1292,7 @@ impl Context {
contact_id: Option<ContactId>,
) -> String {
match protect {
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
ProtectionStatus::Unprotected => {
if let Some(contact_id) = contact_id {
chat_protection_disabled(self, contact_id).await
} else {

View File

@@ -31,7 +31,7 @@ async fn test_verified_oneonone_chat_not_broken_by_device_change() {
check_verified_oneonone_chat_protection_not_broken(false).await;
}
async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_email: bool) {
async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -42,7 +42,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
if broken_by_classical_email {
if by_classical_email {
tcm.section("Bob uses a classical MUA to send a message to Alice");
receive_imf(
&alice,
@@ -58,7 +58,6 @@ async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_
.await
.unwrap()
.unwrap();
// Bob's contact is still verified, but the chat isn't marked as protected anymore
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(contact.is_verified(&alice).await.unwrap(), true);
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
@@ -199,7 +198,6 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
let chat_id = tcm.execute_securejoin(bob, alice).await;
let chat = Chat::load_from_db(bob, chat_id).await?;
assert!(chat.is_protected());
assert!(!chat.is_protection_broken());
Ok(())
}
@@ -213,7 +211,6 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
// A chat with an unknown contact should be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
receive_imf(
&alice,
@@ -230,14 +227,12 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
// Now Bob is a known contact, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
tcm.send_recv(&bob, &alice, "hi").await;
chat.id.delete(&alice).await.unwrap();
// Now we have a public key, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
Ok(())
}
@@ -525,7 +520,6 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
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);
Ok(())
}
@@ -812,19 +806,15 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
if protected != ProtectionStatus::ProtectionBroken {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(contact.is_verified(this).await.unwrap(), true);
}
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(contact.is_verified(this).await.unwrap(), true);
let chat = this.get_chat(other).await;
let (expect_protected, expect_broken) = match protected {
ProtectionStatus::Unprotected => (false, false),
ProtectionStatus::Protected => (true, false),
ProtectionStatus::ProtectionBroken => (false, true),
};
assert_eq!(chat.is_protected(), expect_protected);
assert_eq!(chat.is_protection_broken(), expect_broken);
assert_eq!(
chat.is_protected(),
protected == ProtectionStatus::Protected
);
assert_eq!(chat.is_protection_broken(), false);
}
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {

View File

@@ -0,0 +1,65 @@
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17"
MIME-Version: 1.0
From: <alice@example.org>
To: <bob@example.net>
Subject: [...]
Date: Mon, 28 Jul 2025 14:15:14 +0000
Message-ID: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
References: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
Chat-Version: 1.0
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkgQQFggAOgUCaIeF8RYhBC5vossjtTLXKGNLWGSw
j2Gp7ZRDAhsDAh4BBQsJCAcCBhUKCQgLAgQWAgMBAScCGQEACgkQZLCPYantlEM66gD/b9qi1/H1Cr
UwwlW2akVX86Q0gX6isyKfuNu/CdTdzaQBAIHRxvwlBNZr56qMGL7CyVy6LmBslLlbQwAdclM9t9UE
uDgEXlh13RIKKwYBBAGXVQEFAQEHQAbtyNbLZIUBTwqeW2W5tVbrusWLJ+nTUmtF7perLbYdAwEIB8
J4BBgWCAAgBQJoh4XxAhsMFiEELm+iyyO1MtcoY0tYZLCPYantlEMACgkQZLCPYantlEPG2QD8DthL
48j1wnjw+Kby7CmAm/M+Me82izk8dGNPn442jJ4A/2r+YmqfUPK2XDXPRwvVBAIz5bL44fe7gNkUUu
XMnzkP
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
Content-Type: application/pgp-encrypted; charset="utf-8"
Content-Description: PGP/MIME version identification
Content-Transfer-Encoding: 7bit
Version: 1
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
Content-Type: application/octet-stream; name="encrypted.asc";
charset="utf-8"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc";
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
wV4D5tq63hTeebASAQdAg7f2cyYQy+7Xrrlq4j3ik2Ba7L2sbh7Tt398Kke65Rkw
sUscOFFConkBj7T5D8XS3e9dX5Bnf1z5jTj15OUZx/2iPTRQFtoVoRB6k6vt/qwq
wcBMA+PY3JvEjuMiAQf/U9yB7kWhDMmFI7pMoktqqIvO8woKQG0dx2v1tzjIdtY/
KlqMW8qpAqMHHalInGjj0LDDv4iWnNf8yTh71FyGQgipqH4FnYmbRoFW8iicMixN
0ps6c6tBiZmyWDq2Ub9SIs9L/W2+vDlbyvFnow08MitOniZLC79KXB2ZRFwp7kOm
1gqsZVvsy9fSM7oxXrxAtu2VNyp18emd6jAFWYAz2ISl2KuWYlmSJVexSvQWMRz/
IkKWf9kOVAEuIeUnNo45S/DVb9uWQN3M1TrIdJ1BC+nEDBJokCWUGES6kchJc823
EkLWpn6JDyRYV/7S9tmYrXodO3x4bSL02OnFbyUjutLCRwEduCtRzg5yrw4IAvJO
1ujWf7CaAYE+49oh530HZ/gnBJb03nJhj1SOV0qO9ZquczaW0lhSEtfQF1lVWAVc
BCE44YoR9sBqiJEJ0Msj/WLlso5RZHvHa64JrNJ7Jvisgn5vCMSfInzQ4zIZ7LfD
sR444bJS9V6MNDSuhKmvPvu4wCFZgNQPs4V51yBX8Rjpn/3xws+NpUtisTt5J+ji
KOQg3Thy/9NaNmuXHRbPBxzJKdHzL0bctzVxxDyZPcg6Z2Iteea4gQLEwp5HHw2R
VMX97vtamsjp++tMihXRnrwX/a7x9MCAFuzZted4fB87VjHIdhf+CN2KshWsX+X5
rPR3+oB6EBVXt8IroGMYLTtmMBS4SzEyiGmNFe/Z4tQSU6pEH+Aeo/FmUhUaMhln
BAgRRmhw1Mt9nnuRzLwstpN4W5+mnmccNVg0T6kZz9D7Rbjd7FdzgF8d5K1cJiY/
Nv5aajaFKSEwAO9TNHNoP3LD5KxMPiCkRh888V3YhCOwTUfwJG8riWgeyFCN6Xor
7k6qHhd3T+1u8QTQkooLWSR7UYu9upQzExvmRPNyAXFyLrZUYjlymC1vn9PfH3Pd
31aCGYaYPMdyenoAWTwy7VVSR3wpJuzwHHMeowzCA4TklD/tr2mZSpUrgeBqvS6s
k68Pi5WjMs/kH/3Wl5Octb8XYN++DiG7RH5JzWYRchURen8jgPjzJPIUI5t+C8w0
vXycuP1PdJcSfKTgxkaQgLs5cUoKEAgO5fA9bUPmjEcizb89im6SoObB+6o7hfwa
AIr0TjpOmkdL3TANYA5448gTR4Kq+FwhsxX+fHU6OxwxLBozMcBzvjReKdJko8D+
joaTEZBFxyvQUub5/MXmuulTEDhwURgGMbIN0TukdYlhUBfvyJ/wl/U9aHWvk+dz
3OJ6d9SqTKPPyluTPV7p3GEDy1AwAex5FrP8SxRGRHiMjVhlbwrQB89ZcUX376ge
5MPc4wBn44baPluklYcQtk6kp62KuLpfuLT8VbiLDfKT2FoZzoAnUnw=
=HN9M
-----END PGP MESSAGE-----
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17--

View File

@@ -0,0 +1,123 @@
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716"
MIME-Version: 1.0
From: <alice@example.org>
To: <bob@example.net>, <fiona@example.net>,
<alice@example.org>
Subject: [...]
Date: Thu, 1 Jan 1970 00:00:00 +0000
Message-ID: <rfc724_mid>
Chat-Version: 1.0
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkgQQFggAOgUCaIDT6RYhBC5vossjtTLXKGNLWGSw
j2Gp7ZRDAhsDAh4BBQsJCAcCBhUKCQgLAgQWAgMBAScCGQEACgkQZLCPYantlEO5iQEAkKyQJYbI/n
zh1IplGOQoJDLgObT8W0oicissPmwBskwBANT8jxTqS+y/8dXObksXNgDnByU5SAXe9o3QeeNKqKoN
uDgEXlh13RIKKwYBBAGXVQEFAQEHQAbtyNbLZIUBTwqeW2W5tVbrusWLJ+nTUmtF7perLbYdAwEIB8
J4BBgWCAAgBQJogNPpAhsMFiEELm+iyyO1MtcoY0tYZLCPYantlEMACgkQZLCPYantlEMrlQD+KJrE
wIdwx6fCj6bAuqigKQMsV7If8Bctcv9Wk7QFK1kA/20HB1kPRTUSHyG+6UDyT2g40MOYwllmMMdHV6
rCXGEG
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716
Content-Type: application/pgp-encrypted; charset="utf-8"
Content-Description: PGP/MIME version identification
Content-Transfer-Encoding: 7bit
Version: 1
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716
Content-Type: application/octet-stream; name="encrypted.asc";
charset="utf-8"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc";
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
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
=a+ak
-----END PGP MESSAGE-----
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716--