Compare commits

...
Author SHA1 Message Date
Hocuri a981f9ef68 Revert "--wip-- [skip ci]"
This reverts commit 1355112e5a4d657404f8499e3cfbc61f45371048.
2026-05-13 22:01:44 +02:00
Hocuri a8b5171825 --wip-- [skip ci] 2026-05-13 22:01:44 +02:00
Hocuri 450b2f4610 iequidoo's review 2026-05-13 22:01:44 +02:00
Hocuriandiequidoo 7d432c7fb4 Update src/message.rs
Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2026-05-13 22:01:44 +02:00
Hocuriandiequidoo f617e5ced7 Update deltachat-jsonrpc/src/api.rs
Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2026-05-13 22:01:44 +02:00
Hocuri 6c862ed298 Update provider database 2026-05-13 22:01:44 +02:00
Hocuri 0e3897c5ef More small tweaks after self-rewiewing 2026-05-13 22:01:44 +02:00
Hocuri 13c58670da Restore one of the removed tests 2026-05-13 22:01:44 +02:00
Hocuri 5e02f3ca53 small comment tweaks 2026-05-13 22:01:44 +02:00
Hocuri e4eb798c17 Next try to fix test_send_and_receive_message_markseen() and test_mdn_asymmetric() 2026-05-13 22:01:44 +02:00
Hocuri 7fb8949b60 Try to fix test_basic_imap_api() 2026-05-13 22:01:43 +02:00
Hocuri 10f151522e Remove test_verified_group_vs_delete_server_after() because the feature it tests was removed 2026-05-13 22:01:43 +02:00
Hocuri 95afe23887 Try to fix test_webxdc_message(), test_send_and_receive_message_markseen(), test_mdn_asymmetric() 2026-05-13 22:01:43 +02:00
Hocuri 2bc53fa717 Try to fix test_delete_multiple_messages(), remove some commented-out code, add some comments 2026-05-13 22:01:43 +02:00
Hocuri d315257fb1 Fix test_moved_markseen() 2026-05-13 22:01:43 +02:00
Hocuri 7ba752b5e6 Fix test_markseen_message_and_mdn 2026-05-13 22:01:43 +02:00
Hocuri 850b782493 Fix test_trash_multiple_messages() 2026-05-13 22:01:43 +02:00
Hocuri e29d21876d Linters 2026-05-13 22:01:43 +02:00
Hocuri f174d14f74 Mark message for deletion right after receiving it 2026-05-13 22:01:43 +02:00
Hocuri 411519d5a6 Remove delete_server_after code. Rust tests are passing 2026-05-13 22:01:43 +02:00
Hocuri 4bdc3c29ed docs: Update README.md: Use ci-chatmail instead of nine (#8238)
IIRC, using nine.testrun.org for testing is bad, and ci-chatmail should
be used instead?
2026-05-13 21:50:45 +02:00
link2xt 439216c12c feat: log all connection attempt errors instead of the first one
We log all connection attempts errors as they fail already,
but once all attempts are exhausted, we only log the first error
without specifying which address failed.

The first error is frequently the least interesting
"Network is unreachable (os error 101)" that happens
when trying to connect to IPv6 address from 
a network that does not support IPv6.

To make reading the logs easier,
log all errors together with the addresses
again once all connection attempts are exhausted.
Then it will be visible that IPv6 failed
with "Network is unreachable (os error 101)"
and IPv4 failed with "Connection timeout: deadline has elapsed"
or similar error.

Before the change error looked like this:

    IMAP failed to connect to example.org:143:starttls: Connection failure: Network is unreachable (os error 101).

With the change the error looks like this:

    IMAP failed to connect to example.org:143:starttls: All connection attempts failed: Connection to [***::1]:143 failed: Network is unreachable (os error 101); Connection to [***::2]:143 failed: Network is unreachable (os error 101); Connection to x.x.x.1:143 timed out: deadline has elapsed; Connection to x.x.x.2:143 timed out: deadline has elapsed; Connection to x.x.x.3:143 timed out: deadline has elapsed.
2026-05-13 18:38:18 +00:00
link2xt 0bb4c3d073 feat: enable draft-pqc feature on pgp crate
This is needed to have support of v6 PQC keys
by the time users start generating profiles
using such keys.

Test key was generated with rsop/v0.10.0-16-gd98265f
(commit d98265f821e7bb181d06da1d634c5c4668d89e83)
using the command
cargo run --features draft-pqc generate-key \
          --profile draft-ietf-openpgp-pqc-14-v6-ed25519-mlkem768x25519
2026-05-12 21:56:05 +00:00
link2xt 64f65886f6 chore: update zerocopy from 0.7.32 to 0.7.35
Otherwise dependency resolver fails
if you enable "draft-pqc" feature on "pgp" crate.
2026-05-12 21:56:05 +00:00
link2xt 8658f938e5 test: set email addresses explicitly for the test accounts
v6 keys will not have User IDs, so email addresses
cannot be extracted from there.
2026-05-12 21:56:05 +00:00
link2xt f75b4abefe feat: remove Content-Description and Content-Disposition from multipart/encrypted parts
This is not required by <https://datatracker.ietf.org/doc/html/rfc3156>.

Looks like Content-Description and Content-Disposition are
the same as what Thunderbird produces,
e.g. in `test-data/message/thunderbird_encrypted_signed.eml`,
and the same values also made it into Autocrypt spec.

Content-Description is likely not used by anyone.

For Content-Disposition specification see <https://datatracker.ietf.org/doc/html/rfc2183>.
While it is explicitly allowed to set the filename for inline attachment,
it does not look useful to specify generic "encrypted.asc" explicitly.
Content-Disposition for the second part results in some webmail clients
showing the second part as an attachment with "encrypted.asc" filename.
They will likely show it differently after this change,
e.g. with a different filename or actually inline.
2026-05-12 21:32:30 +00:00
link2xt f2f2dd42ed chore: clippy::useless-borrows-in-formatting fixes 2026-05-12 21:28:45 +00:00
link2xt 8132f32e91 chore: allow passing arguments to scripts/clippy.sh
This makes it possible to run `scripts/clippy.sh --fix --allow-dirty`
to fix clippy warnings automatically.
2026-05-12 16:28:22 +00:00
dependabot[bot] 3d28951885 chore(deps): bump cachix/install-nix-action from 31.10.5 to 31.10.6
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.10.5 to 31.10.6.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](https://github.com/cachix/install-nix-action/compare/ab739621df7a23f52766f9ccc97f38da6b7af14f...8aa03977d8d733052d78f4e008a241fd1dbf36b3)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.10.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 16:27:52 +00:00
dependabot[bot] bc2d624a80 chore(deps): bump taiki-e/install-action from 2.75.19 to 2.77.1
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.19 to 2.77.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/5f57d6cb7cd20b14a8a27f522884c4bc8a187458...cca35edeb1d01366c2843b68fc3ca441446d73d3)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.77.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 16:27:21 +00:00
link2xt fa8fcaaa2b refactor: use some more let..else 2026-05-12 16:26:38 +00:00
link2xt 31c74d82bd fix: fix migration 152
It was converting OutFailed to OutDraft
instead of converting OutPreparing to OutFailed.
2026-05-11 20:53:00 +00:00
link2xt 6fb2f27831 Revert "fix: set dir to "auto" in body tag when converting plain-text to HTML (#8227)"
This reverts commit 58a09df49a
which was merged with failing CI.
2026-05-08 22:11:33 +02:00
adb 58a09df49a fix: set dir to "auto" in body tag when converting plain-text to HTML (#8227)
close #8223
2026-05-08 20:20:00 +02:00
iequidoo ca70fb9b3a feat: Get rid of MessageState::{OutPreparing,OutMdnRcvd} in the db
`OutPreparing` is deprecated since 2024-12-07, replace it with `OutFailed`, such messages are
probably not interesting anymore. `OutMdnRcvd` is not used in the db for new messages since
a30c6ae1f7, `OutDelivered` is stored instead.
2026-05-07 20:37:11 -03:00
iequidoo 045b586569 feat: Never remove primary transport when applying SyncTransports message
If we missed a message changing the primary transport, we shouldn't remove it when applying
a SyncTransports message, such a state doesn't look correct even if it's temporary.
2026-05-07 20:04:22 -03:00
iequidooandHocuri 18e1ecbb94 fix: Generate new pre-message Message-ID when forwarding
Otherwise if it's forwarded to a device that already has the original
message, the pre-message isn't received and this results in a message
having no text and `Forwarded` param.

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-05-07 19:35:03 -03:00
iequidoo 6fdee2b92d docs: Remove outdated comment about "quota warning" device message
The device message was removed in 7de58f5329.
2026-05-07 19:31:38 -03:00
dependabot[bot] 9ebd4769f5 chore(cargo): bump openssl from 0.10.78 to 0.10.79
Bumps [openssl](https://github.com/rust-openssl/rust-openssl) from 0.10.78 to 0.10.79.
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-v0.10.78...openssl-v0.10.79)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.79
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 09:13:45 -03:00
dependabot[bot] 741d1beed8 chore(cargo): bump data-encoding from 2.10.0 to 2.11.0
Bumps [data-encoding](https://github.com/ia0/data-encoding) from 2.10.0 to 2.11.0.
- [Commits](https://github.com/ia0/data-encoding/compare/v2.10.0...v2.11.0)

---
updated-dependencies:
- dependency-name: data-encoding
  dependency-version: 2.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-04 14:10:58 -03:00
dependabot[bot] ac8b2d2fca chore(cargo): bump colorutils-rs from 0.7.6 to 0.8.0
Bumps [colorutils-rs](https://github.com/awxkee/colorutils-rs) from 0.7.6 to 0.8.0.
- [Release notes](https://github.com/awxkee/colorutils-rs/releases)
- [Commits](https://github.com/awxkee/colorutils-rs/compare/0.7.6...0.8.0)
2026-05-04 13:42:52 -03:00
link2xt d75c05e717 ci: do not store Rust cache from PRs
<https://github.com/swatinem/rust-cache>
caches only dependencies, and
dependencies don't change most of the time,
so PRs store the same cache
as already stored
by the main branch commit PRs are based on.

Hash of Cargo.{toml,lock} is part of the
cache key, so for dependency updating PRs
the cache key is new. Such PRs
rebuild everything from scratch,
which is a separate problem.
Storing such cache from PR is however
not useful because most of the time
dependency updating PR is going
to be rebased again before merging
as Dependabot PRs are opened in a batch
and then merged one by one
while Dependabot rebases remaining PRs.
2026-05-04 11:21:33 +00:00
dependabot[bot] 35ceb51ffc chore(cargo): bump libc from 0.2.184 to 0.2.186
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.184 to 0.2.186.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.186/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.184...0.2.186)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-04 04:16:07 +00:00
iequidoo 8492f84b13 refactor: Don't temporarily extend signatures for signed-only messages 2026-05-03 23:35:40 -03:00
dependabot[bot] e713c231eb chore(cargo): bump tokio from 1.50.0 to 1.52.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.50.0 to 1.52.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.50.0...tokio-1.52.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-04 00:06:32 +00:00
dependabot[bot] 80c9ca44ca chore(cargo): bump hyper from 1.8.1 to 1.9.0
Bumps [hyper](https://github.com/hyperium/hyper) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.8.1...v1.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-03 21:46:04 +00:00
link2xt 6e04993e75 chore(deny.toml): add cpufeatures duplicate dependency exception 2026-05-03 20:13:23 +00:00
dependabot[bot] 4529ed2240 chore(cargo): bump blake3 from 1.8.3 to 1.8.5
Bumps [blake3](https://github.com/BLAKE3-team/BLAKE3) from 1.8.3 to 1.8.5.
- [Release notes](https://github.com/BLAKE3-team/BLAKE3/releases)
- [Commits](https://github.com/BLAKE3-team/BLAKE3/compare/1.8.3...1.8.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-03 18:22:54 +00:00
link2xt b5ebd6f686 chore: add exceptions for hickory-proto 0.25.2 in deny.toml
Cannot be updated and nothing serious even if possible to trigger.
2026-05-03 18:22:23 +00:00
iequidooandHocuri 4d537544ef fix: Emit MsgsChanged, not IncomingMsg, for messages only having special parts (#8157)
An example of such messages is location-only messages.

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-04-30 22:02:06 -03:00
iequidoo 4a16c0c3dd test: EventTracker::get_matching_opt: Return the first matching event, not last 2026-04-30 22:02:06 -03:00
Hocuri 4c01802982 feat: Remove mostly-unused SignUnencrypted option (#8190) 2026-04-30 13:59:30 +02:00
Hocuri 4b528e426b docs: Discourage into(), try_into() and parse() (#8180)
Follow-up to
https://github.com/chatmail/core/pull/8178#issuecomment-4322738959

In a previous version, I added a note that the JsonRPC API is a notable exception, but I removed it.
2026-04-30 13:58:19 +02:00
link2xt 585de7d18b docs: update echobot_no_hooks.py example
Use add_or_update_transport() instead of deprecated configure(),
do not use deprecated get_next_messages(),
make use of AttrDict,
print invite link,
do not use % formatting for logging.
2026-04-29 17:14:45 +00:00
link2xt 0598fdcab3 chore: update astral-tokio-tar from 0.6.0 to 0.6.1
Fixes https://rustsec.org/advisories/RUSTSEC-2026-0112
2026-04-29 17:05:38 +00:00
Nico de Haen 903e736fa2 fix: default value for imap folder (#8193)
resolves #8192
2026-04-29 08:24:13 +02:00
iequidoo f20907d597 feat: is_self_addr(): Employ the config cache to optimize for ConfiguredAddr passed 2026-04-28 23:41:25 -03:00
B. Petersen 804590c7f3 api: jsonrpc: remove unused set_draft_vcard() 2026-04-28 17:12:16 +02:00
dependabot[bot] 62d4cf4ed8 chore(deps): bump taiki-e/install-action from 2.75.10 to 2.75.19
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.10 to 2.75.19.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/85b24a67ef0c632dfefad70b9d5ce8fddb040754...5f57d6cb7cd20b14a8a27f522884c4bc8a187458)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 14:16:35 +00:00
link2xt 0d772d4dba api!(deltachat-rpc-client): remove deprecated get_fresh_messages_in_arrival_order() 2026-04-28 14:16:10 +00:00
dependabot[bot] 408afa5656 chore(deps): bump cachix/install-nix-action from 31.9.1 to 31.10.5
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.9.1 to 31.10.5.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](https://github.com/cachix/install-nix-action/compare/2126ae7fc54c9df00dd18f7f18754393182c73cd...ab739621df7a23f52766f9ccc97f38da6b7af14f)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.10.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 14:15:29 +00:00
link2xt 1a6249c10f build: increase MSRV to 1.89
This is required by iroh 0.98.1,
so we will need to update MSRV eventually
when iroh 1.0 is released.
2026-04-28 14:14:47 +00:00
Nico de Haen daea820fe5 chore(json-rpc): deprecate send_sticker (#8189)
send_sticker is not needed anymore since
https://github.com/chatmail/core/pull/8162/changes/4dbbd4d8e
2026-04-28 15:59:51 +02:00
Hocuri b806efa096 refactor: Make Fingerprint not implement Display (#8177)
Currently, the Fingerprint type implements Display, but this doesn't get
you the canonical fingerprint representation, but something
human-readable. This is confusing, and back when I first used
`Fingerprint`, I immediately wrote a bug because of this. So, instead,
make a function `human_readable()` on Fingerprint.

This comes from the discussion at
https://github.com/chatmail/core/pull/8174#discussion_r3143130722.
2026-04-27 11:22:21 +02:00
Hocuri 0580056b62 refactor: Use regular functions rather than FromStr impls (#8178)
Implementing `FromStr` and then calling `parse()` creates an
indirection, which is hard to follow for people who are not familiar
with Rust. @r10s recently voiced this problem when we were
pair-programming, and I agree.

We can decide what exactly to call the new function.

I didn't remove all `FromStr` implementations yet; `FromStr for
Fingerprint`, `FromStr for Params`, `FromStr for MozConfigTag` and
`FromStr for EphemeralTimer` are still left.
2026-04-27 11:09:01 +02:00
link2xt 63f96d9138 perf: set location for all accounts in parallel 2026-04-27 03:12:26 +00:00
link2xt 1204a94252 perf: stop sending locations concurrently 2026-04-27 03:12:26 +00:00
link2xt 73e8bee120 api!(location): avoid repeating module name in function names
E.g. rename location::is_sending_locations_to_chat to location::is_sending_to_chat.
Then import only the location module and use it with location:: prefix,
similarly to how e.g. std::fs or std::tokio::fs avoids using "fs" or "file"
in function names.
2026-04-27 03:12:26 +00:00
link2xt 8c927c7f86 api: add JSON-RPC APIs for location streaming
New API stop_sending_locations() only available
in JSON-RPC stops location streaming
in all accounts and chats.
2026-04-27 03:12:26 +00:00
link2xt 7f9c184659 refactor: split is_sending_locations_to_chat() into two functions 2026-04-27 03:12:26 +00:00
iequidoo 287d730556 chore: Apply rustmft after the previous commit 2026-04-26 12:04:18 -03:00
iequidoo 82bb77b056 feat: Drop support for replacing partial download stubs 2026-04-26 12:04:18 -03:00
Hocuri aa1f129a48 refactor: Use self_fingerprint() where it makes sense (#8174) 2026-04-26 09:36:08 +02:00
Hocuri 0ad58f7a59 test: Remove unused test data related to Authentication-Result parsing (#8175)
Follow-up to https://github.com/chatmail/core/pull/8172
2026-04-26 09:35:45 +02:00
iequidooandHocuri 6d61f7e071 fix: Don't receive message if a deletion request was received before (#8143)
There's no check for `from_id` so another group member can delete the
message, but this can only happen in case of message reordering and the
problem already exists for usual messages, so we may ignore it and
overall a group represents a scope of trust.

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-04-25 21:19:03 -03:00
Hocuri fa68c1f0e4 refactor: Remove mostly-unused function get_secondary_self_addrs() (#8173) 2026-04-25 23:37:36 +02:00
Hocuri 5f1d54100f refactor: Remove unused Authentication-Results parsing (#8172) 2026-04-25 23:33:15 +02:00
Hocuri 25cd7b65fd api!: Remove unused info_only option when loading a chatlist (#8171)
Remove unused info_only option from `get_chat_msgs_ex()`.

This option was meant to show an "audit log" of a group, i.e. only the
info messages. This feature was removed again from Desktop, but the
option still lingered around in Core.

This also adds a doc comment to the JsonRPC functions, because I
wanted to note somewhere that the parameter is deprecated, and I needed
some place to put this note.
2026-04-25 23:30:53 +02:00
link2xt 63596a4940 feat: remove show_emails config 2026-04-25 20:24:57 +00:00
link2xt 8bc84e13de test: use Displayname instead of ShowEmails for config cache test
ShowEmails is gonig to be removed.
2026-04-25 20:24:57 +00:00
B. Petersen ba8c39ff5b Revert "api: add clear_all_relay_storage API"
This reverts commit 10ab556d65.
2026-04-25 22:15:19 +02:00
B. Petersen 7de58f5329 feat: adapt quota warning to automatic cleanup 2026-04-25 21:50:13 +02:00
Hocuri 1fd4a19e56 feat: Don't show non-delivery-notfications in broadcast channels (#8159)
Looking forward, it is unclear how exactly we want to handle errors, but
in the meantime, this is a simple thing we can do to improve the
situation.

If you have a broadcast channel, chances are that some of your
recipients have some problems receiving your messages. You are probably
not too interested in that.

In reality, some channel operators were quite confused by these errors,
and we told them to just ignore them, but at this point we may as well
just hide them.

Theoretically we can start removing recipients that never get our
messages at some point.
2026-04-25 17:19:47 +00:00
link2xt 1ab6645bbc api!: remove dc_delete_all_locations 2026-04-24 20:31:09 +00:00
link2xt c17d067a1a refactor: remove unnecessary async block in dc_set_location 2026-04-24 20:29:49 +00:00
link2xt 3aeb2d44b7 feat: remove non-sticker heuristics and force_sticker()
This causes problems for Delta Chat Desktop at
<https://github.com/deltachat/deltachat-desktop/pull/6278>
even though the logic was originally introduced for iOS.
If the problem remains on iOS,
heuristics can be added into iOS UI.
2026-04-24 18:18:51 +00:00
dependabot[bot] d069e75cd8 chore(cargo): bump openssl from 0.10.72 to 0.10.78
Bumps [openssl](https://github.com/rust-openssl/rust-openssl) from 0.10.72 to 0.10.78.
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-v0.10.72...openssl-v0.10.78)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.78
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 05:08:01 -03:00
link2xt ad5e904d1c chore(cargo): update rustls-webpki to 0.103.13
Also ignore RUSTSEC-2026-0104 because iroh 0.35.0
pulls in rustls-webpki that cannot be updated.
2026-04-22 18:10:40 +00:00
iequidoo 38affa2c62 fix: Don't resort re-sent message to the bottom (#8145)
We shouldn't just revert ad7f873c68 because then we won't be able to
normally transfer a backup from a device having clock a bit in the future.

INDEXED BY msgs_index7 is to prevent SQLite from downgrading to using msgs_index2 which has less
ordering.
2026-04-22 13:54:56 -03:00
link2xt 6dfc6f8780 chore: update provider database 2026-04-22 12:27:28 +00:00
dependabot[bot] 8cca0cf75d chore(deps): bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e...cef221092ed1bacb1cc03d23a2d87d1d172e277b)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-22 10:46:18 +00:00
dependabot[bot] b81f50be8f chore(deps): bump zizmorcore/zizmor-action from 0.5.2 to 0.5.3
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.2 to 0.5.3.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/71321a20a9ded102f6e9ce5718a2fcec2c4f70d8...b1d7e1fb5de872772f31590499237e7cce841e8e)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-22 10:45:09 +00:00
Hocuriandiequidoo 970222f376 feat: Resend the last 10 messages to new broadcast member (#8151)
Last 10 messages in a broadcast channel are resent. They are sent and encrypted only to the new member, not to other subscribers.

Close #7678

Based on https://github.com/chatmail/core/pull/7854, with the following
changes:

- Refactor and simplify code, don't reuse the existing `get_chat_msgs()`
function
- Document that Param::Arg4 is also used for resent messages
cc818d9099
- It's unclear how exactly to resend webxdc status updates. After
discussing with @r10s, don't resend webxdc's at all for now.
38d57ebb30
- Don't set fake `msg_id` in resent messages
e7d0687d90
Setting the msg_id to `u32::MAX` is hacky, and may just as well break
    things as it may fix things, because some code may use the msg.id to
    load information from the database, like `get_iroh_topic_for_msg()`.
    From reading the code, I couldn't find any problem with leaving the
    correct `msg_id`, and if there is one, then we should add a function
parameter `is_resending` that is checked in the corresponding places.

Easiest to review file-by-file rather than individual commits, probably.
I'll squash-merge this.

---------

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2026-04-21 22:34:53 +02:00
link2xt 83e31a5f17 fix: add error cause to connectivity view for IMAP errors
For SMTP errors we already format `last_send_error` with {:#},
but for IMAP errors we have formatted the errors with .to_string().
This resulted in errors such as
"Error: IMAP failed to connect to example.org:443:tls"
instead of
"Error: IMAP failed to connect to example.org:443:tls: Connection failure: Network is unreachable (os error 101)."
in the connectivity view HTML.
2026-04-21 19:08:33 +00:00
iequidoo 31fabb24df feat: Don't send Chat-Group-Name* headers for InBroadcast-s
Broadcast subscribers can't change the chat name, so sending the "Chat-Group-Name{,-Timestamp}"
headers looks unnecessary. That could be useful for other subscriber's devices, but having only the
chat name isn't enough anyway, at least knowing the secret is necessary which is sent by the
broadcast owner.
2026-04-21 08:42:10 -03:00
Hocuri 66df0d2a3c api: Deprecate old server config keys that were replaced by add_or_update_transport() 2026-04-20 15:45:17 +02:00
Hocuri 5a6b1c62dd refactor: Rename EnteredLoginParam::load() and save() to load_legacy() and save_legacy() 2026-04-20 15:45:17 +02:00
Hocuri 18d878378f api!: Remove unused config smtp_certificate_checks 2026-04-20 15:45:17 +02:00
link2xt 3c25e4b726 api: add clear_all_relay_storage API 2026-04-19 13:01:47 +00:00
link2xt 8cd06bb785 fix: use write transaction in SpkiHashStore.cleanup()
query_map_vec() uses read-only connection,
so it cannot be used to delete rows.
2026-04-19 09:15:13 +00:00
link2xt bb816ff398 fix: do not sort prefetched messages by INTERNALDATE
Messages are iterated over in fetch_new_msg_batch()
and largest_uid_fetched variable is updated there
assuming that messages come in the order of increasing UID.
If UIDs are not increasing, it is possible
that largest_uid_fetched will be updated
even though smaller UID is not fetched yet
and the message will be lost.

INTERNALDATE sorting was introduced to
deal with email providers such as Gmail
that keep INTERNALDATE but not the UID
order when moving the messages.
Since we don't move the messages anymore
after commit 04c0e7da16,
there is no need for ordering by INTERNALDATE.
2026-04-19 09:00:40 +00:00
link2xt 9fcb26c849 chore(cargo): upgrade rand 0.8.5 to rand 0.8.6
This upgrade resolves RUSTSEC-2026-0097
2026-04-19 09:00:00 +00:00
B. Petersen d9474a678e fix python test 2026-04-18 23:45:35 +02:00
B. Petersen f1e1a240ac feat: webxdc sending contexts 2026-04-18 23:45:35 +02:00
link2xt 82924952fb feat: allow TLS connections with invalid certificate if the key is unchanged
This change weakens TLS checks.
Every time we make a successful TLS connection,
we remember public key hash from the certificate
in relation to the hostname.
If later we connect to the same hostname and the public key does not change,
we skip checking certificate chain.
This way we will still connect successfully
even if certificate expires or becomes invalid for another reason,
but keeps the key.

We always check that certificate corresponds to the hostname.
We also do this for certificates starting with _
where we allow self-signed certificates,
so self-signed certificates with mismatching domains are not allowed.
Previously we did not check this for domains starting with _.
2026-04-17 18:07:20 +00:00
link2xt 7daa6cc8d9 ci: update Rust to 1.95.0 2026-04-17 15:24:24 +00:00
link2xt b8cfee7e9e build: remove coredeps Dockerfile
It was used to build legacy CFFI Python binding wheels.
We are not building them anymore and it is not
tested that they can still be built.
2026-04-17 15:24:24 +00:00
link2xt 03fc2d26ee ci: remove Concourse CI pipelines
They were used to build legacy Python bindings,
but we stopped updating them and have no Concourse CI setup.
2026-04-17 15:24:24 +00:00
link2xt 942172a31a feat: remove MvboxMove and OnlyFetchMvbox 2026-04-16 16:42:40 +00:00
link2xt 04c0e7da16 feat: do not unconditionally watch mvbox for non-chatmail
Since commit 25750de4e1
released in 2.36.0 we do not move messages to mvbox without explicit
mvbox_move setting, so do not need to watch it as well
as long as other devices are updated to the same change.
2026-04-16 16:42:40 +00:00
iequidoo 4178671839 fix: Scale up contacts messaged in groups to IncomingTo
This makes such contacts appear in the contact list. `IncomingTo` is used because
`ChatId::accept_ex()` does so for groups, so sending a group message is effectively accepting the
group again in regards to contact searchability.

This fixes up b549e7633d which made it impossible to find contacts
from groups even if we've written there.
2026-04-16 12:56:41 -03:00
dependabot[bot] 49e8065b4c chore(deps): bump swatinem/rust-cache from 2.8.2 to 2.9.1
Bumps [swatinem/rust-cache](https://github.com/swatinem/rust-cache) from 2.8.2 to 2.9.1.
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/swatinem/rust-cache/compare/779680da715d629ac1d338a641029a2f4372abb5...c19371144df3bb44fab255c43d04cbc2ab54d1c4)

---
updated-dependencies:
- dependency-name: swatinem/rust-cache
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 15:41:17 +00:00
Hocuri 1c24ad91eb feat: Remove the largely-unused ability to send multiple reactions to one message (#8131)
After talking with r10s:

For spring cleaning, remove the largely-unused things that can be done a
bit.

Most private messengers (WhatsApp/Signal/...) do not have this feature,
and we do not want to become Matrix where every client has different,
partly-incompatible features.
2026-04-16 14:30:05 +00:00
link2xt 8f7777b843 ci: upgrade cargo-deny-action to v2.0.17 2026-04-16 16:14:12 +02:00
link2xt caeddcd57b chore: add exception for RUSTSEC-2026-0097 2026-04-16 16:14:12 +02:00
link2xt 418fd92d6d build: upgrade rustls-webpki to 0.103.12
Ignore <https://rustsec.org/advisories/RUSTSEC-2026-0098>
and <https://rustsec.org/advisories/RUSTSEC-2026-0099>
for rustls-webpki 0.102.8 that is an iroh dependency.
2026-04-16 16:14:12 +02:00
iequidoo a70924a1d6 fix: fetch_single_msg(): Lock fetch_msgs_mutex before fetching
This is to avoid race conditions in `receive_imf` which also happened previously when
multi-transport was added.
2026-04-16 10:40:16 -03:00
iequidoo 8e91eecb3d feat: decide_chat_assignment: Log correct post_msg_exists value
rfc724_mid_exists() returns the message id not only when the message is fully downloaded, so we need
to check this additionally. Also improve test_receive_pre_message to check that receiving the
pre-message again doesn't affect the existing message.
2026-04-16 10:40:16 -03:00
iequidooandHocuri 89f948028d fix: Set Param::GuaranteeE2ee before preparing message blob (#8090)
Otherwise `Param::GuaranteeE2ee` is set only after rendering the message
and some UIs, e.g. DC Android, display the message as unencrypted while
preparing the blob and rendering and encrypting the message. NB: DC
Desktop doesn't display the message until `send_msg()` returns.

---------
Co-authored-by: Hocuri <hocuri@gmx.de>
2026-04-15 11:28:53 -03:00
72374 9adb71bf8f feat: Change multiplier to 7/8 when scaling down avatars
The resolution-limits for avatar-images are currently 512x512 or 256x256.
Reducing the resolution further, to 2/3 each step, can reduce the quality
much more than is necessary to fit within the file-size-limits,
which are currently 60 kB or 20 kB.

An image made entirely of noise (which results in unusually
large file-sizes), encoded with jpeg-quality 75,
and 4:2:2-colour-subsampling (the format currently used
for encoding images), can be below 60 kB at 227x227.
For the lower file-size-limit of 20 kB,
such images can be too large at 170x170, but fit at 149x149.
More normal images will have a lower file-size at the same resolution.

Before this change, the target-resolutions for resampling were:
512x512 ->                       341x341 ->                       227x227.

And for the lower file-size-limit:
256x256 ->                       170x170 ->                       113x113.

After this change, the target-resolutions for resampling will be:
512x512 -> 448x448 -> 392x392 -> 343x343 -> 300x300 -> 262x262 -> 229x229.

And for the lower file-size-limit, those will be:
256x256 -> 224x224 -> 196x196 -> 171x171 -> 149x149 -> 130x130 -> 113x113.

This does add 2 steps between the previous target-resolutions,
while still reaching target-resolutions close to the previous ones,
to reduce situations in which the quality will be lower than before.
2026-04-15 13:57:41 +02:00
72374 fe2ba05804 feat: Increase the resolution-limit WORSE_AVATAR_SIZE from 128 to 256
The file-size of many images will already be smaller than 20 kB,
when encoded at 256x256, and it can be a large improvement in quality.
2026-04-15 13:57:41 +02:00
link2xt 072fc34c77 fix: trash no-op messages about self being added to groups 2026-04-15 07:02:15 +00:00
link2xt cb00bd7043 refactor: replace HashSet with BTreeSet
This is needed to avoid golden tests being flaky
because of info message order being different
between runs.
2026-04-15 07:02:15 +00:00
link2xt f766c11075 perf: enable clippy::large_futures lint
Large size of Mimefactor.render() futures
increases the size of all callers
down to set_config() and background_fetch().
I also had to Box::pin one call to fetch_new_msg_batch
and one call to fetch_single_msg.
2026-04-15 05:33:52 +00:00
link2xt 89e450894d refactor: make HTML parser non-async 2026-04-14 12:43:46 +00:00
DarkCat09 6ef1f7d52b fix: restart io on transport deletion
Fixes #8038
2026-04-14 13:11:28 +02:00
dependabot[bot] 33dc3d20ad chore(deps): bump taiki-e/install-action from 2.64.0 to 2.74.0
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.64.0 to 2.74.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/69e777b377e4ec209ddad9426ae3e0c1008b0ef3...85b24a67ef0c632dfefad70b9d5ce8fddb040754)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.74.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 03:36:39 +00:00
dependabot[bot] b8a0c37efe chore(cargo): bump rand from 0.9.2 to 0.9.3
Bumps [rand](https://github.com/rust-random/rand) from 0.9.2 to 0.9.3.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.9.3/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.9.3)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.9.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 03:36:12 +00:00
iequidoo ad7f873c68 fix: Ensure that message being sent is added to the bottom (#8027)
Before, if the user fixed their clock incorrectly set to the future, they needed to delete
previously sent messages or wait until this future comes again so that new sent messages are added
to the bottom. Strictly speaking, the problem isn't fixable because we don't know if messages were
incorrectly timestamped into the future or they are timestamped correctly and the clock is now
incorrectly set to the past. Anyway, adding messages to the middle of the chat isn't a good way to
inform the user about the problem.
2026-04-13 08:49:58 -03:00
link2xt 3236c8bbf4 chore: bump version to 2.50.0-dev 2026-04-13 10:14:01 +02:00
link2xt dab7ca19fe chore(release): prepare for 2.49.0 2026-04-13 10:10:07 +02:00
DarkCat09 520cd0ede8 docs: fix broken link for i-d "Common PGP/MIME Message Mangling" 2026-04-12 00:32:51 +00:00
WofWca 5d5deedec3 refactor: less nested remove_contact_from_chat 2026-04-11 12:23:16 +04:00
link2xt f33e21ccb9 fix: trash message about group name change from non-member 2026-04-11 01:26:51 +00:00
link2xt 00c06c490b test: use TestContextManager in test_keep_member_list_if_possibly_nomember 2026-04-11 01:26:51 +00:00
Hocuri 8b58b16cb5 fix: For bots, wait with emitting IncomingMsg until the Post-Msg arrived (#8104)
I used some AI to draft a first version of this, and then reworked it.

This is one of multiple possibilities to fix
https://github.com/chatmail/core/issues/8041: For bots, the IncomingMsg
event is not emitted when the pre-message arrives, only when the
post-message arrives. Also, post-messages are downloaded immediately,
not after all the small messages are downloaded.

The `get_next_msgs()` API is deprecated. Instead, bots need to listen to
the IncomingMsg event in order to be notified about new events. Is this
acceptable for bots?

THE PROBLEM THAT WAS SOLVED BY THIS:

With pre-messages, it's hard for bots to wait for the message to be fully downloaded and then process it.

Up until now, bots used get_next_msgs() to query the unprocessed messages, then set last_msg_id after processing a message to that they won't process it again.

But this will now also return messages that were not fully downloaded.

ALTERNATIVES:

In the following, I will explain
the alternatives, and for why it's not so easy to just make the
`get_next_msgs()` API work. If it's not understandable, I'm happy to
elaborate more.

Core can't just completely ignore the pre-message for two reasons:
- If a post-message containing a Webxdc arrives later, and some webxdc updates arrive in the meantime, then these updates will be lost.
- The post-message doesn't contain the text (reasoning was to avoid duplicate text for people who didn't upgrade yet during the 2.43.0 rollout)

There are multiple solutions:
- Add the message as hidden in the database when the pre-message arrives.
  - When the post-message arrives and we want to make it available for bots, we need to update the msg_id because of how the `get_next_msgs()` API works. This means that we need to update all webxdc updates that reference this msg_id.
  - Alternatively, we could make webxdc's reference the rfc724_mid instead of the msg_id, so that we don't need stable msg_ids anymore
  - Alternatively, we could deprecate `get_next_msgs()`, and ask bots to use plain events for message processing again. It's not that bad; worst case, the bot crashes and then forgets to react to some messages, but the user will just try again. And if some message makes the bot crash, then it might actually be good not to try and process it again.
- Store the pre-message text and `PostMsgMetadata` (or alternatively, the whole mime) in a new database table. Wait with processing it until the post-message arrives.

Additionally, the logic that small messages are downloaded before post-messages should be disabled for bots, in order to prevent reordering.
2026-04-10 21:10:46 +02:00
link2xt d6971ee4ac fix: make start messages stick to the top of the chat
We already set sort_timestamp to 0 for "Messages are end-to-end encrypted."
since 8f1bf963b4.
Do this for "Others will only see this group after you sent a first message."
and "Messages in this chat use classic email and are not encrypted." as well
so no messages can be added on top.
2026-04-10 03:16:12 +00:00
DavidSM100 e3bf6bf352 refactor(@deltachat/stdio-rpc-server): remove await from README example 2026-04-09 14:58:55 +00:00
DavidSM100 4b81cd2fc8 api(@deltachat/stdio-rpc-server): also export a class
This is convenient for bots and libs for bots, so they can extend from this class directly
2026-04-09 14:58:55 +00:00
DavidSM100 be920bf3bf refactor(@deltachat/stdio-rpc-server): make getRPCServerPath and startDeltaChat synchronous 2026-04-09 14:58:55 +00:00
link2xt 602f0a088e ci: make sure -dev version suffix is not forgotten after release
Workflow checks that PRs are made only when current version ends with -dev
If this fails, a commit bumping the version to -dev should be pushed to main branch.
2026-04-09 14:34:23 +00:00
link2xt a2bb8962cb fix: add missing extern "C" to dc_array_is_independent
It was the only `unsafe fn` (not `unsafe extern "C" fn`) in lib.rs
2026-04-09 03:24:15 +00:00
link2xt 795fe9a38b chore: bump version to 2.49.0-dev 2026-04-08 22:27:29 +02:00
holger krekel 60bc4011f7 fix: let search also return hidden contacts if search value is an email address 2026-04-07 22:39:13 +02:00
link2xt f552cf93b4 fix: assign webxdc updates from post-message to webxdc instance 2026-04-07 19:14:44 +00:00
link2xt f75a7986b5 refactor: ignore ForcePlaintext in saved messages chat
ForcePlaintext was used for Autocrypt Setup Message,
there is no need to support it in saved messages chat anymore.
2026-04-07 17:00:47 +00:00
link2xt 3b8f1934f3 api!: remove dc_msg_force_plaintext
Message.force_plaintext() is still used in legacy SecureJoin steps
internally, so cannot be removed, but there is no need for public API.
2026-04-07 17:00:47 +00:00
dependabot[bot] c8716f50aa chore(deps): bump dependabot/fetch-metadata from 2.4.0 to 3.0.0
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.4.0 to 3.0.0.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.4.0...v3.0.0)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 05:15:35 +00:00
link2xt d2097d3523 fix: do not URL-encode proxy hostnames 2026-04-06 13:16:28 +00:00
link2xt 1219cbe1a3 fix: do not create 1:1 chat on second device when scanning a QR code
This avoids creating 1:1 chat on a second device when joining a channel.
Now when joining a channel there may be no 1:1 chat with the inviter
when the channel is created. In this case we still create the channel
as unblocked even if 1:1 chat would be a contact request
because joining the channel is an explicit action
and it is not possible to add someone who did not scan a QR
to the channel manually.
2026-04-06 00:42:14 +00:00
iequidoo bc48b17e93 test: Fix flaky test_no_old_msg_is_fresh: Wait for incoming message before sending outgoing one
We don't want to send an outgoing message from the 2nd device (`ac1_clone`) before receiving the
incoming one and expect that the messages will be ordered correctly on the 1st device (`ac1`). Let's
ensure the correct message order locally in the first place. I checked logs of a failed test run and
it indeed happened that `ac1_clone` sent the message earlier, so it can't reference the incoming
message and `tweak_sort_timestamp()` does nothing on `ac1`, so the messages can't be ordered
correctly considering that smeared clocks on the devices are diferent.
2026-04-05 20:59:46 -03:00
Hocuri 7233b4b811 test: Test that messages are only marked as delivered after being fully sent out (#8077)
Test for https://github.com/chatmail/core/pull/8062. I checked that the
test fails without #8062.
2026-04-05 20:37:32 +00:00
iequidoo d1e0088201 feat: Flipped Exif orientations (#8057)
Before, sending of images flipped in Exif led to images having wrong orientation.
2026-04-05 17:04:17 -03:00
dependabot[bot] a5e41b0b49 chore(cargo): bump proptest from 1.10.0 to 1.11.0
Bumps [proptest](https://github.com/proptest-rs/proptest) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/proptest-rs/proptest/releases)
- [Changelog](https://github.com/proptest-rs/proptest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/proptest-rs/proptest/compare/v1.10.0...v1.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 17:03:06 -03:00
iequidoo 2f76fd98dd test: Add test for tweak_sort_timestamp()
The part of logic there adjusting the sort timestamp forward if the parent message has a greater
sort timestamp wasn't tested explicitly by any test. I only saw one unrelated "golden test" failure
when commented it out.
(Related to #8027)
2026-04-05 11:16:15 -03:00
dependabot[bot] 6235f2a01a chore(cargo): bump image from 0.25.9 to 0.25.10
Bumps [image](https://github.com/image-rs/image) from 0.25.9 to 0.25.10.
- [Changelog](https://github.com/image-rs/image/blob/v0.25.10/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.9...v0.25.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 05:50:41 +00:00
dependabot[bot] ec5117a6c2 chore(cargo): bump quote from 1.0.44 to 1.0.45
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.44 to 1.0.45.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.44...1.0.45)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 15:45:56 +00:00
dependabot[bot] d6e3a8829b chore(cargo): bump libc from 0.2.182 to 0.2.183
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.182 to 0.2.183.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.183/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.182...0.2.183)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 15:45:37 +00:00
dependabot[bot] 2340818488 chore(cargo): bump tokio from 1.49.0 to 1.50.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.49.0 to 1.50.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.49.0...tokio-1.50.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 15:45:07 +00:00
dependabot[bot] f175d2fed9 chore(cargo): bump pin-project from 1.1.10 to 1.1.11
Bumps [pin-project](https://github.com/taiki-e/pin-project) from 1.1.10 to 1.1.11.
- [Release notes](https://github.com/taiki-e/pin-project/releases)
- [Changelog](https://github.com/taiki-e/pin-project/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/pin-project/compare/v1.1.10...v1.1.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:20:27 +00:00
dependabot[bot] d318bbb0f4 chore(cargo): bump tempfile from 3.26.0 to 3.27.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.26.0 to 3.27.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.26.0...v3.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:15:16 +00:00
dependabot[bot] a0f14a5978 chore(cargo): bump tracing-subscriber from 0.3.22 to 0.3.23
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.22 to 0.3.23.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.22...tracing-subscriber-0.3.23)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:14:51 +00:00
dependabot[bot] 7e49033f92 chore(cargo): bump chrono from 0.4.43 to 0.4.44
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.43 to 0.4.44.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.43...v0.4.44)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:10:54 +00:00
Hocuri 626ac8161a fix: Mark a message as delivered only after it has been fully sent out (#8062)
Fix https://github.com/chatmail/core/issues/8042

The problem was that after receiving the bcc_self'ed pre-message in
`receive_imf`, the logic there only looked for a pending
`smtp`-table-entry that matches the rfc724_mid, and if there was none
then it thought "Great, apparently the message is fully sent out, we can
mark it as delivered!".

But with pre-messages, the same message can have two `smtp` entries (one
for the pre-message and one for the post-message), and the message
should only be marked as delivered once both of them are sent out.

Now, I changed the logic to look for all entries with the same msg_id.
This is actually the same SQL query used in smtp.rs, so, I extracted it
into a new function; feel free to suggest a better name for it.

I tested on Android that it now works fine.

I'll add a test in a follow-up PR.

There are a lot of other problems with sending large files, though:
- The pre-message is sent before the post-message, so that for the
receiver it looks as if the message arrived, but stays in
"downloading..." forever
- There is quite a time delay between clicking on "Send" and the
outgoing message appearing in the chat
- The message shortly gets a letter icon right after it is sent
- I'm wondering if there is a way to give feedback to the user
immediately if the message is too big
- It's unclear when exactly we want to send read receipts

I'll open a follow-up issue for these.
2026-04-02 15:12:17 +02:00
holger krekel 28cce5e31d fix: determine whether a message is an own message by looking at signature. multiple devices can temporarly have different sets of self addresses, and still need to properly recognize incoming versus outgoing messages. Disclaimer: some LLM tooling was initially involved but i went over everything by hand, and also addressed review comments. 2026-04-01 14:51:48 +02:00
link2xt 3b87e27f34 docs: document that events are broadcasted to all event emitters
Delta Chat for iOS already relies on this behavior,
so it cannot be practically changed.
2026-04-01 09:03:07 +00:00
link2xt 24b21c0588 chore(release): prepare for 2.48.0 2026-03-30 12:48:24 +02:00
link2xt eb666d4cc3 test: the message is sorted correctly in the chat even if it arrives late 2026-03-30 08:52:19 +00:00
link2xt ef265689dd fix: do not sort received messages below the last seen one 2026-03-30 08:52:19 +00:00
link2xt 49223792f9 fix: never sort the message before chat joining timestamp
This is to avoid sorting incoming messages that
are slightly in the past above system messages
about SecureJoin. SecureJoin messages are
timed according to smeared timestamp,
so even in the local tests they are in the future
by a few seconds.
2026-03-30 08:52:19 +00:00
Hocuri 920da083d1 fix: Manipulate sort_timestamp to not be 0 2026-03-30 08:52:19 +00:00
link2xt 8f1bf963b4 fix: always sort "Messages are end-to-end encrypted" notice to the beginning
We set timestamp of this info message to 0
to make it always appear in the beginning of the chat.
To avoid new chats being sorted to the end of the chatlist,
we ignore such 0 and use chat creation timestamp
when sorting the chatlist.
2026-03-30 08:52:19 +00:00
link2xt e33d50b4e0 test: use load_imf_email() more 2026-03-30 08:52:19 +00:00
link2xt f1dc03a4ee test: do not rely on loading newest chat in load_imf_email()
We know which message was added from the return value
of receive_imf(). It may be that the first chat
in the chatlist is not the one where the message was received
if there is a pinned chat or if
just received message is old.
2026-03-30 08:52:19 +00:00
link2xt 5d90cc7a2a test: remove test_old_message_5
It is not clear now what this is testing.
Golden test shows messages ordered
incorrectly according to the timestamps,
they should be ordered the other way round.

Comment talks about fetching from mvbox and inbox
in paralell which is a rare case that
could have happened if one message is left in the inbox
and the other message is a chat message moved to mvbox.
We never download anything that is not moved to the target folder.

The test also resides in "verified chats" tests
which are all legacy tests we kept after
replacing the concept of verified/protected chats
with key contacts in 2.x.
2026-03-30 08:52:19 +00:00
link2xt 68e630eb82 fix: remove migration 108
This removes migration added in 625887d249
2026-03-30 08:38:28 +00:00
iequidoo ef718bb869 fix: When receiving MDN, mark all preceding messages as noticed, even having same timestamp (#7928)
This fixes flaky JSON-RPC `test_multidevice_sync_seen`.
2026-03-29 11:50:50 -03:00
iequidoo f1860f90d4 feat: Log received message sort timestamp
This way it's easier to debug issues like `MsgsNoticed` not emitted for a chat.
2026-03-29 11:50:50 -03:00
link2xt a947f4296f refactor(securejoin): do not check for self address in forwarding protection
If our key is gossiped, the message is intended for us.
The check for address is redundant for incoming messages as
if we received the message then it was addressed to us.

This whole protection code can eventually be removed
as we have intended recipient fingerprints already,
it only protects against forwarding of messages
sent by old clients.
2026-03-28 16:20:39 +00:00
link2xt 8c3139f7a2 feat: add decryption error to the device message about outgoing message decryption failure 2026-03-28 13:27:15 +00:00
link2xt 3dd7defaa1 docs: add SQL performance tips to STYLE.md 2026-03-28 10:08:54 +00:00
link2xt 3096dd6027 ci: fix https://docs.zizmor.sh/audits/#bot-conditions 2026-03-28 09:29:12 +01:00
link2xt ee62d2d281 ci: use environment for js.jsonrpc.delta.chat deployment 2026-03-28 01:06:12 +01:00
link2xt 6095971f67 ci: use environment for cffi.delta.chat deployment 2026-03-27 16:37:58 +01:00
link2xt 32ff5b7a6b ci: use environment for rs.delta.chat deployment 2026-03-27 09:36:11 +00:00
link2xt b87805ab24 fix: cleanup imap and imap_sync records without transport in housekeeping
Previously transports deleted via sync messages left unused `imap` entries.
2026-03-26 16:24:11 +00:00
link2xt c8716ad85a fix: delete imap_markseen entries not corresponding to any imap rows 2026-03-26 16:24:11 +00:00
link2xt 4dd0ba2c72 fix: move sorting outside of SQL query in store_seen_flags_on_imap
With `ORDER BY` statement SQLite searches
the `imap` table by `transport_id` and for each found row
scans the whole `imap_markseen` table.
Number of `imap` entries for each `transport_id`
is usually large as we need to know
which UIDs to delete on IMAP server
when deleting a message.

```
sqlite> EXPLAIN QUERY PLAN
SELECT imap.id, uid, folder FROM imap, imap_markseen
WHERE imap.id = imap_markseen.id
AND imap.transport_id=?
AND target = folder
ORDER BY folder, uid;
QUERY PLAN
|--SEARCH imap USING INDEX sqlite_autoindex_imap_1 (transport_id=?)
`--SCAN imap_markseen
```

Without `ORDER BY` statement SQLite scans `imap_markseen`
table which is expected to be small,
and then searches `imap` table by `rowid` for each found result.

```
sqlite> EXPLAIN QUERY PLAN
SELECT imap.id, uid, folder FROM imap, imap_markseen
WHERE imap.id = imap_markseen.id
AND imap.transport_id=?
AND target = folder;
QUERY PLAN
|--SCAN imap_markseen
`--SEARCH imap USING INTEGER PRIMARY KEY (rowid=?)
```

Query planning was tested with SQLite 3.52.0.
It is possible to explictly make
query planner move sorting to the last step
with `ORDER +folder, +uid`, but this is not recommended
in SQLite documentation
(see <https://www.sqlite.org/optoverview.html#uplus>).

It is also possible to add indexes,
but indexes use space,
adding them requires an SQL migration,
and each index needs to be updated so it will slow down writes.
2026-03-26 16:24:11 +00:00
link2xt a24248a90b ci: update {c,py}.delta.chat website deployments
The host has been changed and the secrets are moved to environments.
2026-03-26 15:25:53 +00:00
iequidoo af16fc9038 fix: Make Message-ID of pre-messages stable across resends (#8007) 2026-03-25 23:32:33 -03:00
473 changed files with 4054 additions and 7335 deletions
+19 -9
View File
@@ -20,10 +20,10 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.94.0
RUST_VERSION: 1.95.0
# Minimum Supported Rust Version
MSRV: 1.88.0
MSRV: 1.89.0
jobs:
lint_rust:
@@ -40,7 +40,9 @@ jobs:
- run: rustup override set $RUST_VERSION
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run rustfmt
run: cargo fmt --all -- --check
- name: Run clippy
@@ -59,7 +61,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb
with:
arguments: --workspace --all-features --locked
command: check
@@ -91,7 +93,9 @@ jobs:
show-progress: false
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Rustdoc
run: cargo doc --document-private-items --no-deps
@@ -134,10 +138,12 @@ jobs:
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@69e777b377e4ec209ddad9426ae3e0c1008b0ef3
uses: taiki-e/install-action@cca35edeb1d01366c2843b68fc3ca441446d73d3
with:
tool: nextest
@@ -168,7 +174,9 @@ jobs:
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build C library
run: cargo build -p deltachat_ffi
@@ -194,7 +202,9 @@ jobs:
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build deltachat-rpc-server
run: cargo build -p deltachat-rpc-server
+8 -8
View File
@@ -34,7 +34,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -58,7 +58,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
@@ -82,7 +82,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -106,7 +106,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
@@ -157,7 +157,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -181,7 +181,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
@@ -208,7 +208,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v7
@@ -382,7 +382,7 @@ jobs:
- name: Publish deltachat-rpc-server to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
+2 -2
View File
@@ -10,11 +10,11 @@ permissions:
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.4.0
uses: dependabot/fetch-metadata@v3.0.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR
+23
View File
@@ -0,0 +1,23 @@
# Check that PRs are made against the -dev version.
#
# If this fails, push commit to update the version to -dev to main.
name: Check for -dev version
on:
pull_request:
permissions: {}
jobs:
check_dev_version:
name: Check that current version ends with -dev
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Run version-checking script
run: scripts/check-dev-version.py
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
with:
node-version: 18.x
- name: Add Rust cache
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
- name: npm install
working-directory: deltachat-jsonrpc/typescript
run: npm install
+3 -3
View File
@@ -25,7 +25,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix fmt flake.nix -- --check
build:
@@ -84,7 +84,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -105,5 +105,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix build .#${{ matrix.installable }}
@@ -47,4 +47,4 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary
+45 -12
View File
@@ -1,16 +1,18 @@
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, py.delta.chat and cffi.delta.chat
on:
push:
branches:
- main
- build_jsonrpc_docs_ci
permissions: {}
jobs:
build-rs:
runs-on: ubuntu-latest
environment:
name: rs.delta.chat
url: https://rs.delta.chat/
steps:
- uses: actions/checkout@v6
@@ -23,12 +25,15 @@ jobs:
- name: Upload to rs.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:/var/www/html/rs.delta.chat/"
build-python:
runs-on: ubuntu-latest
environment:
name: py.delta.chat
url: https://py.delta.chat/
steps:
- uses: actions/checkout@v6
@@ -36,18 +41,21 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:/var/www/html/py.delta.chat"
build-c:
runs-on: ubuntu-latest
environment:
name: c.delta.chat
url: https://c.delta.chat/
steps:
- uses: actions/checkout@v6
@@ -55,18 +63,22 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:/var/www/html/c.delta.chat"
build-ts:
runs-on: ubuntu-latest
environment:
name: js.jsonrpc.delta.chat
url: https://js.jsonrpc.delta.chat/
defaults:
run:
working-directory: ./deltachat-jsonrpc/typescript
@@ -90,6 +102,27 @@ jobs:
- name: Upload to js.jsonrpc.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
build-cffi:
runs-on: ubuntu-latest
environment:
name: cffi.delta.chat
url: https://cffi.delta.chat/
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:/var/www/html/cffi.delta.chat/"
-31
View File
@@ -1,31 +0,0 @@
# GitHub Actions workflow
# to build `deltachat_ffi` crate documentation
# and upload it to <https://cffi.delta.chat/>
name: Build & Deploy Documentation on cffi.delta.chat
on:
push:
branches:
- main
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+119
View File
@@ -1,5 +1,122 @@
# Changelog
## [2.49.0] - 2026-04-13
### Features / Changes
- Flipped Exif orientations ([#8057](https://github.com/chatmail/core/pull/8057)).
### Fixes
- Determine whether a message is an own message by looking at signature. multiple devices can temporarly have different sets of self addresses, and still need to properly recognize incoming versus outgoing messages. Disclaimer: some LLM tooling was initially involved but i went over everything by hand, and also addressed review comments..
- Mark a message as delivered only after it has been fully sent out ([#8062](https://github.com/chatmail/core/pull/8062)).
- Do not create 1:1 chat on second device when scanning a QR code.
- Do not URL-encode proxy hostnames.
- Assign webxdc updates from post-message to webxdc instance.
- Let search also return hidden contacts if search value is an email address.
- Add missing `extern "C"` to `dc_array_is_independent`.
- Make start messages stick to the top of the chat.
- For bots, wait with emitting IncomingMsg until the Post-Msg arrived ([#8104](https://github.com/chatmail/core/pull/8104)).
- Trash message about group name change from non-member.
### API-Changes
- [**breaking**] remove `dc_msg_force_plaintext`.
- @deltachat/stdio-rpc-server: also export a class.
### CI
- Make sure `-dev` version suffix is not forgotten after release.
### Documentation
- Document that events are broadcasted to all event emitters.
- Fix broken link for i-d "Common PGP/MIME Message Mangling".
### Refactor
- ignore ForcePlaintext in saved messages chat.
- @deltachat/stdio-rpc-server: make `getRPCServerPath` and `startDeltaChat` synchronous.
- @deltachat/stdio-rpc-server: remove `await` from README example.
- less nested `remove_contact_from_chat`.
### Tests
- Add test for `tweak_sort_timestamp()`.
- Test that messages are only marked as delivered after being fully sent out ([#8077](https://github.com/chatmail/core/pull/8077)).
- Fix flaky `test_no_old_msg_is_fresh`: Wait for incoming message before sending outgoing one.
- Use TestContextManager in `test_keep_member_list_if_possibly_nomember`.
### Miscellaneous Tasks
- cargo: bump chrono from 0.4.43 to 0.4.44.
- cargo: bump tracing-subscriber from 0.3.22 to 0.3.23.
- cargo: bump tempfile from 3.26.0 to 3.27.0.
- cargo: bump pin-project from 1.1.10 to 1.1.11.
- cargo: bump tokio from 1.49.0 to 1.50.0.
- cargo: bump libc from 0.2.182 to 0.2.183.
- cargo: bump quote from 1.0.44 to 1.0.45.
- cargo: bump image from 0.25.9 to 0.25.10.
- cargo: bump proptest from 1.10.0 to 1.11.0.
- deps: bump dependabot/fetch-metadata from 2.4.0 to 3.0.0.
- bump version to 2.49.0-dev.
## [2.48.0] - 2026-03-30
### Fixes
- Fix reordering problems in multi-relay setups by not sorting received messages below the last seen one.
- Always sort "Messages are end-to-end encrypted" notice to the beginning.
- Make Message-ID of pre-messages stable across resends ([#8007](https://github.com/chatmail/core/pull/8007)).
- Delete `imap_markseen` entries not corresponding to any `imap` rows.
- Cleanup `imap` and `imap_sync` records without transport in housekeeping.
- When receiving MDN, mark all preceding messages as noticed, even having same timestamp ([#7928](https://github.com/chatmail/core/pull/7928)).
- Remove migration 108 preventing upgrades from core 1.86.0 to the latest version.
### Features / Changes
- Improve IMAP loop logs.
- Add decryption error to the device message about outgoing message decryption failure.
- Log received message sort timestamp.
### Performance
- Move sorting outside of SQL query in `store_seen_flags_on_imap`.
### API-Changes
- Add JSON-RPC API `markfresh_chat()`.
- ffi: Correctly declare `dc_event_channel_new()` as having no params ([#7831](https://github.com/chatmail/core/pull/7831)).
### Refactor
- Remove `wal_checkpoint_mutex`, lock `write_mutex` before getting sql connection instead.
- Replace async `RwLock` with sync `RwLock` for stock strings.
- Cleanup remaining Autocrypt Setup Message processing in `mimeparser`.
- SecureJoin: do not check for self address in forwarding protection.
- Fix clippy warnings.
### CI
- Update {c,py}.delta.chat website deployments.
- Use environments for {rs,cffi,js.jsonrpc}.delta.chat deployments.
- Fix https://docs.zizmor.sh/audits/#bot-conditions.
### Documentation
- Add SQL performance tips to STYLE.md.
### Tests
- Remove `test_old_message_5`.
- Do not rely on loading newest chat in `load_imf_email()`.
- Use `load_imf_email()` more.
- The message is sorted correctly in the chat even if it arrives late.
### Miscellaneous Tasks
- cargo: update rustls-webpki to 0.103.10.
## [2.47.0] - 2026-03-24
### Fixes
@@ -7984,3 +8101,5 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.45.0]: https://github.com/chatmail/core/compare/v2.44.0..v2.45.0
[2.46.0]: https://github.com/chatmail/core/compare/v2.45.0..v2.46.0
[2.47.0]: https://github.com/chatmail/core/compare/v2.46.0..v2.47.0
[2.48.0]: https://github.com/chatmail/core/compare/v2.47.0..v2.48.0
[2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
Generated
+186 -99
View File
@@ -36,7 +36,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -136,7 +136,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"cpufeatures 0.2.17",
"password-hash",
"zeroize",
]
@@ -194,9 +194,9 @@ dependencies = [
[[package]]
name = "astral-tokio-tar"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c23f3af104b40a3430ccb90ed5f7bd877a8dc5c26fc92fde51a22b40890dcf9"
checksum = "4ce73b17c62717c4b6a9af10b43e87c578b0cac27e00666d48304d3b7d2c0693"
dependencies = [
"filetime",
"futures-core",
@@ -497,16 +497,16 @@ dependencies = [
[[package]]
name = "blake3"
version = "1.8.3"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d"
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq 0.4.2",
"cpufeatures",
"cpufeatures 0.3.0",
]
[[package]]
@@ -550,7 +550,7 @@ dependencies = [
"bolero-kani",
"bolero-libfuzzer",
"cfg-if",
"rand 0.9.2",
"rand 0.9.4",
]
[[package]]
@@ -573,7 +573,7 @@ dependencies = [
"bolero-generator",
"lazy_static",
"pretty-hex",
"rand 0.9.2",
"rand 0.9.4",
"rand_xoshiro",
]
@@ -799,7 +799,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -827,9 +827,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.43"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"num-traits",
@@ -934,9 +934,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorutils-rs"
version = "0.7.6"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2fc25857fa523662de5cae84225b0e7bfb24a2a3f9ed8802fecf03df7252b1"
checksum = "69abc9a8ed011e2b7946769f460b9e76e8b659ece9ef4001b9d8bba3489f796d"
dependencies = [
"erydanos",
"half",
@@ -1011,6 +1011,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.2.1"
@@ -1216,7 +1225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
@@ -1292,9 +1301,9 @@ dependencies = [
[[package]]
name = "data-encoding"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]]
name = "dbl"
@@ -1307,7 +1316,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.48.0-dev"
version = "2.50.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1360,8 +1369,8 @@ dependencies = [
"proptest",
"qrcodegen",
"quick-xml",
"rand 0.8.5",
"rand 0.9.2",
"rand 0.8.6",
"rand 0.9.4",
"ratelimit",
"regex",
"rusqlite",
@@ -1416,7 +1425,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.48.0-dev"
version = "2.50.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1437,7 +1446,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.48.0-dev"
version = "2.50.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1453,7 +1462,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.48.0-dev"
version = "2.50.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1482,7 +1491,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.48.0-dev"
version = "2.50.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1490,7 +1499,7 @@ dependencies = [
"human-panic",
"libc",
"num-traits",
"rand 0.9.2",
"rand 0.9.4",
"serde_json",
"thiserror 2.0.18",
"tokio",
@@ -2429,7 +2438,7 @@ dependencies = [
"idna",
"ipnet",
"once_cell",
"rand 0.9.2",
"rand 0.9.4",
"ring",
"thiserror 2.0.18",
"tinyvec",
@@ -2451,7 +2460,7 @@ dependencies = [
"moka",
"once_cell",
"parking_lot",
"rand 0.9.2",
"rand 0.9.4",
"resolv-conf",
"smallvec",
"thiserror 2.0.18",
@@ -2600,10 +2609,29 @@ dependencies = [
]
[[package]]
name = "hyper"
version = "1.8.1"
name = "hybrid-array"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9"
dependencies = [
"typenum",
]
[[package]]
name = "hybrid-array"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af"
dependencies = [
"typenum",
"zeroize",
]
[[package]]
name = "hyper"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
"atomic-waker",
"bytes",
@@ -2616,7 +2644,6 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
@@ -2654,7 +2681,7 @@ dependencies = [
"hyper",
"libc",
"pin-project-lite",
"socket2 0.6.0",
"socket2 0.6.3",
"tokio",
"tower-service",
"tracing",
@@ -2852,7 +2879,7 @@ dependencies = [
"hyper",
"hyper-util",
"log",
"rand 0.9.2",
"rand 0.9.4",
"tokio",
"url",
"xmltree",
@@ -2860,9 +2887,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.9"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -2981,7 +3008,7 @@ dependencies = [
"pin-project",
"pkarr",
"portmapper",
"rand 0.8.5",
"rand 0.8.6",
"rcgen",
"reqwest",
"ring",
@@ -3056,7 +3083,7 @@ dependencies = [
"iroh-metrics",
"n0-future",
"postcard",
"rand 0.8.5",
"rand 0.8.6",
"rand_core 0.6.4",
"serde",
"serde-error",
@@ -3119,7 +3146,7 @@ checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535"
dependencies = [
"bytes",
"getrandom 0.2.16",
"rand 0.8.5",
"rand 0.8.6",
"ring",
"rustc-hash",
"rustls",
@@ -3172,7 +3199,7 @@ dependencies = [
"pin-project",
"pkarr",
"postcard",
"rand 0.8.5",
"rand 0.8.6",
"reqwest",
"rustls",
"rustls-webpki 0.102.8",
@@ -3246,7 +3273,17 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
name = "kem"
version = "0.3.0-pre.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f"
dependencies = [
"rand_core 0.6.4",
"zeroize",
]
[[package]]
@@ -3260,9 +3297,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.182"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libm"
@@ -3453,13 +3490,42 @@ dependencies = [
[[package]]
name = "mio"
version = "1.0.3"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
"windows-sys 0.61.1",
]
[[package]]
name = "ml-dsa"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac4a46643af2001eafebcc37031fc459eb72d45057aac5d7a15b00046a2ad6db"
dependencies = [
"const-oid",
"hybrid-array 0.3.1",
"num-traits",
"pkcs8",
"rand_core 0.6.4",
"sha3",
"signature",
"zeroize",
]
[[package]]
name = "ml-kem"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f"
dependencies = [
"hybrid-array 0.2.3",
"kem",
"rand_core 0.6.4",
"sha3",
"zeroize",
]
[[package]]
@@ -3483,9 +3549,9 @@ dependencies = [
[[package]]
name = "moxcms"
version = "0.7.5"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
@@ -3776,7 +3842,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
"serde",
"smallvec",
"zeroize",
@@ -3933,15 +3999,14 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.72"
version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
@@ -3974,9 +4039,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.107"
version = "0.9.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
@@ -4198,6 +4263,8 @@ dependencies = [
"k256",
"log",
"md-5",
"ml-dsa",
"ml-kem",
"nom 8.0.0",
"num-bigint-dig",
"num-traits",
@@ -4206,7 +4273,7 @@ dependencies = [
"p256",
"p384",
"p521",
"rand 0.8.5",
"rand 0.8.6",
"regex",
"replace_with",
"ripemd",
@@ -4216,6 +4283,7 @@ dependencies = [
"sha2",
"sha3",
"signature",
"slh-dsa",
"smallvec",
"snafu",
"twofish",
@@ -4235,18 +4303,18 @@ dependencies = [
[[package]]
name = "pin-project"
version = "1.1.10"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.10"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
"proc-macro2",
"quote",
@@ -4412,7 +4480,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
@@ -4424,7 +4492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
@@ -4453,7 +4521,7 @@ dependencies = [
"nested_enum_utils",
"netwatch",
"num_enum",
"rand 0.8.5",
"rand 0.8.6",
"serde",
"smallvec",
"snafu",
@@ -4615,13 +4683,13 @@ dependencies = [
[[package]]
name = "proptest"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532"
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
dependencies = [
"bitflags 2.11.0",
"num-traits",
"rand 0.9.2",
"rand 0.9.4",
"rand_chacha 0.9.0",
"rand_xorshift",
"regex-syntax",
@@ -4701,7 +4769,7 @@ dependencies = [
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand 0.9.2",
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls",
@@ -4729,9 +4797,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
@@ -4776,9 +4844,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
@@ -4787,9 +4855,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.2"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
@@ -5164,7 +5232,7 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.10",
"rustls-webpki 0.103.13",
"subtle",
"zeroize",
]
@@ -5201,9 +5269,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.10"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"ring",
"rustls-pki-types",
@@ -5322,7 +5390,7 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22c3b0257608d7de4de4c4ea650ccc2e6e3e45e3cd80039fcdee768bcb449253"
dependencies = [
"rand 0.9.2",
"rand 0.9.4",
"substring",
"thiserror 1.0.69",
"url",
@@ -5508,7 +5576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -5519,7 +5587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -5547,7 +5615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -5581,7 +5649,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project",
"rand 0.9.2",
"rand 0.9.4",
"sendfd",
"serde",
"serde_json",
@@ -5611,7 +5679,7 @@ dependencies = [
"chacha20poly1305",
"hkdf",
"md-5",
"rand 0.9.2",
"rand 0.9.4",
"ring-compat",
"sha1",
]
@@ -5680,6 +5748,25 @@ dependencies = [
"autocfg",
]
[[package]]
name = "slh-dsa"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267"
dependencies = [
"const-oid",
"digest",
"hmac",
"hybrid-array 0.3.1",
"pkcs8",
"rand_core 0.6.4",
"sha2",
"sha3",
"signature",
"typenum",
"zerocopy",
]
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -5725,12 +5812,12 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.6.0"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -5843,7 +5930,7 @@ dependencies = [
"precis-core",
"precis-profiles",
"quoted-string-parser",
"rand 0.9.2",
"rand 0.9.4",
]
[[package]]
@@ -5870,7 +5957,7 @@ dependencies = [
"hex",
"parking_lot",
"pnet_packet",
"rand 0.8.5",
"rand 0.8.6",
"socket2 0.5.9",
"thiserror 1.0.69",
"tokio",
@@ -5988,9 +6075,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.26.0"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.3",
@@ -6144,9 +6231,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.49.0"
version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"bytes",
"libc",
@@ -6154,7 +6241,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.6.0",
"socket2 0.6.3",
"tokio-macros",
"windows-sys 0.61.1",
]
@@ -6171,9 +6258,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
@@ -6247,7 +6334,7 @@ dependencies = [
"getrandom 0.3.3",
"http 1.1.0",
"httparse",
"rand 0.9.2",
"rand 0.9.4",
"ring",
"rustls-pki-types",
"simdutf8",
@@ -6403,9 +6490,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -7418,9 +7505,9 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f"
[[package]]
name = "zerocopy"
version = "0.7.32"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
@@ -7428,9 +7515,9 @@ dependencies = [
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
+7 -7
View File
@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "2.48.0-dev"
version = "2.50.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"
rust-version = "1.89"
repository = "https://github.com/chatmail/core"
[profile.dev]
@@ -53,7 +53,7 @@ blake3 = "1.8.2"
brotli = { version = "8", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
colorutils-rs = { version = "0.7.5", default-features = false }
colorutils-rs = { version = "0.8.0", default-features = false }
data-encoding = "2.9.0"
escaper = "0.1"
fast-socks5 = "1"
@@ -78,7 +78,7 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.19.0", default-features = false }
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.39", features = ["escape-html"] }
@@ -103,7 +103,7 @@ thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.6", default-features = false }
astral-tokio-tar = { version = "0.6.1", default-features = false }
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"
@@ -181,7 +181,7 @@ harness = false
anyhow = "1"
async-channel = "2.5.0"
base64 = "0.22"
chrono = { version = "0.4.43", default-features = false }
chrono = { version = "0.4.44", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
@@ -198,7 +198,7 @@ rusqlite = "0.37"
sanitize-filename = "0.6"
serde = "1.0"
serde_json = "1"
tempfile = "3.25.0"
tempfile = "3.27.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.18"
+19
View File
@@ -68,6 +68,12 @@ keyword doesn't help here.
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
When changing complex SQL queries, test them on a new database with `EXPLAIN QUERY PLAN`
to make sure that indexes are used and large tables are not going to be scanned.
Never run `ANALYZE` on the databases,
this makes query planner unpredictable
and may make performance significantly worse: <https://github.com/chatmail/core/issues/6585>
## Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
@@ -155,3 +161,16 @@ are documented.
Follow Rust guidelines for the documentation comments:
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
## Do not use `into()`, `try_into()` or `parse()`
For internal types, implementing `From`, `TryFrom` or `FromStr` is discouraged.
Instead, a `new()` function is recommended.
For external types, prefer using `Type::from()`, `Type::try_from()` or `Type::from_str()`
over `into()`, `try_into()` or `parse()`.
Calling `into()`, `try_into()` or `parse()`
creates an indirection,
which is hard to follow for people who are not familiar with Rust,
or who are not using rust-analyzer.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.48.0-dev"
version = "2.50.0-dev"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+47 -123
View File
@@ -364,18 +364,14 @@ uint32_t dc_get_id (dc_context_t* context);
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
*
* Events are broadcasted to all existing event emitters.
* Events emitted before creation of event emitter
* are not available to event emitter.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per context.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* may or may not be available to event emitter.
*/
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
@@ -394,27 +390,9 @@ char* dc_get_blobdir (const dc_context_t* context);
/**
* Configure the context. The configuration is handled by key=value pairs as:
*
* - `addr` = Email address to use for configuration.
* If dc_configure() fails this is not the email address actually in use.
* Use `configured_addr` to find out the email address actually in use.
* - `configured_addr` = Email address actually in use.
* - `configured_addr` = Email address in use.
* Unless for testing, do not set this value using dc_set_config().
* Instead, set `addr` and call dc_configure().
* - `mail_server` = IMAP-server, guessed if left out
* - `mail_user` = IMAP-username, guessed if left out
* - `mail_pw` = IMAP-password (always needed)
* - `mail_port` = IMAP-port, guessed if left out
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `send_server` = SMTP-server, guessed if left out
* - `send_user` = SMTP-user, guessed if left out
* - `send_pw` = SMTP-password, guessed if left out
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `proxy_enabled` = Proxy enabled. Disabled by default.
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
* - `selfavatar` = File containing avatar. Will immediately be copied to the
@@ -430,31 +408,11 @@ char* dc_get_blobdir (const dc_context_t* context);
* 1=send a copy of outgoing messages to self (default).
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `mvbox_move` = 1=detect chat messages,
* move them to the `DeltaChat` folder,
* and watch the `DeltaChat` folder for updates (default),
* 0=do not move chat-messages
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
* `DeltaChat` folder. Messages will still be fetched from the
* spam folder.
* 0=watch all folders normally (default)
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only,
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts (default).
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
* >=1=seconds, after which messages are deleted automatically from the device.
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
* See also dc_estimate_deletion_cnt().
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
* 1=delete messages directly after receiving from server, mvbox is skipped.
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
* "Saved messages" are deleted from the server as well as
* e-mails matching the `show_emails` settings above, the UI should clearly point that out.
* See also dc_estimate_deletion_cnt().
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
* good outgoing images/videos/voice quality at reasonable sizes (default)
* DC_MEDIA_QUALITY_WORSE (1)
@@ -525,6 +483,27 @@ char* dc_get_blobdir (const dc_context_t* context);
* 1 = Contacts (default, does not include contact requests),
* 2 = Nobody (calls never result in a notification).
*
* Also, there are configs that are only needed
* if you want to use the deprecated dc_configure() API, such as:
*
* - `addr` = Email address to use for configuration.
* If dc_configure() fails this is not the email address actually in use.
* Use `configured_addr` to find out the email address actually in use.
* - `mail_server` = IMAP-server, guessed if left out
* - `mail_user` = IMAP-username, guessed if left out
* - `mail_pw` = IMAP-password (always needed)
* - `mail_port` = IMAP-port, guessed if left out
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `send_server` = SMTP-server, guessed if left out
* - `send_user` = SMTP-user, guessed if left out
* - `send_pw` = SMTP-password, guessed if left out
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `proxy_enabled` = Proxy enabled. Disabled by default.
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
* - `imap_certificate_checks` = how to check IMAP and SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* If you want to retrieve a value, use dc_get_config().
*
* @memberof dc_context_t
@@ -550,9 +529,6 @@ int dc_set_config (dc_context_t* context, const char*
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
* - `sys.config_keys` = get a space-separated list of all config-keys available.
* The config-keys are the keys that can be passed to the parameter `key` of this function.
* - `quota_exceeding` = 0: quota is unknown or in normal range;
* >=80: quota is about to exceed, the value is the concrete percentage,
* a device message is added when that happens, however, that value may still be interesting for bots.
*
* @memberof dc_context_t
* @param context The context object. For querying system values, this can be NULL.
@@ -711,6 +687,12 @@ int dc_get_push_state (dc_context_t* context);
/**
* Configure a context.
*
* This way of configuring a context is deprecated,
* and does not allow to configure multiple transports.
* If you can, use the JSON-RPC API (../deltachat-jsonrpc/src/api.rs)
* `add_or_update_transport()`/`addOrUpdateTransport()` instead.
*
* During configuration IO must not be started,
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
* If the context is already configured,
@@ -1400,7 +1382,6 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
#define DC_GCM_ADDDAYMARKER 0x01
#define DC_GCM_INFO_ONLY 0x02
/**
@@ -1421,7 +1402,6 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
* @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.
* 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.
*/
@@ -1476,17 +1456,16 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch
/**
* Estimate the number of messages that will be deleted
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
* by the dc_set_config()-option `delete_device_after`.
* This is typically used to show the estimated impact to the user
* before actually enabling deletion of old messages.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
* @param from_server Deprecated, pass 0 here
* @param seconds Count messages older than the given number of seconds.
* @return Number of messages that are older than the given number of seconds.
* This includes e-mails downloaded due to the `show_emails` option.
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
* Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
*/
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
@@ -2831,19 +2810,6 @@ int dc_set_location (dc_context_t* context, double latit
dc_array_t* dc_get_locations (dc_context_t* context, uint32_t chat_id, uint32_t contact_id, int64_t timestamp_begin, int64_t timestamp_end);
/**
* Delete all locations on the current device.
* Locations already sent cannot be deleted.
*
* Typically results in the event #DC_EVENT_LOCATION_CHANGED
* with contact_id set to 0.
*
* @memberof dc_context_t
* @param context The context object.
*/
void dc_delete_all_locations (dc_context_t* context);
// misc
/**
@@ -3323,18 +3289,14 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const
* This is similar to dc_get_event_emitter(), which, however,
* must not be called for accounts handled by the account manager.
*
* Events are broadcasted to all existing event emitters.
* Events emitted before creation of event emitter
* are not available to event emitter.
*
* @memberof dc_accounts_t
* @param accounts The account manager as created by dc_accounts_new().
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* are not available to event emitter.
*/
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
@@ -4036,8 +3998,6 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
* Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
*
* Outgoing message states:
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
* the message enters this state before @ref DC_STATE_OUT_PENDING. Deprecated.
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
@@ -4250,6 +4210,8 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* true if the Webxdc should get internet access;
* this is the case i.e. for experimental maps integration.
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
* - is_app_sender: Define if the local user is the one who initially shared the webxdc application in the chat.
* - is_broadcast: Define if the app runs in a broadcasting context.
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
@@ -4981,17 +4943,6 @@ uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg);
*/
uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
/**
* Force the message to be sent in plain text.
*
* This API is for bots, there is no need to expose it in the UI.
*
* @memberof dc_msg_t
* @param msg The message object.
*/
void dc_msg_force_plaintext (dc_msg_t* msg);
/**
* @class dc_contact_t
*
@@ -5631,13 +5582,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_STATE_IN_SEEN 16
/**
* Outgoing message being prepared. See dc_msg_get_state() for details.
*
* @deprecated 2024-12-07
*/
#define DC_STATE_OUT_PREPARING 18
/**
* Outgoing message drafted. See dc_msg_get_state() for details.
*/
@@ -5829,7 +5773,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* These constants configure TLS certificate checks for IMAP and SMTP connections.
*
* These constants are set via dc_set_config()
* using keys "imap_certificate_checks" and "smtp_certificate_checks".
* using key "imap_certificate_checks".
*
* @addtogroup DC_CERTCK
* @{
@@ -5979,21 +5923,14 @@ void dc_event_channel_unref(dc_event_channel_t* event_channel);
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
*
* This is similar to dc_get_event_emitter(), which, however,
* must not be called for accounts handled by the account manager.
* Events are broadcasted to all existing event emitters.
* Events emitted before creation of event emitter
* are not available to event emitter.
*
* @memberof dc_event_channel_t
* @param The event channel.
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager / event channel.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* are not available to event emitter.
*/
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);
@@ -6449,8 +6386,7 @@ void dc_event_unref(dc_event_t* event);
* Location of one or more contact has changed.
*
* @param data1 (int) contact_id of the contact for which the location has changed.
* If the locations of several contacts have been changed,
* e.g. after calling dc_delete_all_locations(), this parameter is set to 0.
* If the locations of several contacts have been changed, this parameter is set to 0.
* @param data2 0
*/
#define DC_EVENT_LOCATION_CHANGED 2035
@@ -6721,14 +6657,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_CONFIGURE_PROGRESS || (e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
/*
* Values for dc_get|set_config("show_emails")
*/
#define DC_SHOW_EMAILS_OFF 0
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
#define DC_SHOW_EMAILS_ALL 2
/*
* Values for dc_get|set_config("media_quality")
*/
@@ -7063,11 +6991,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in message summary text for notifications and chatlist.
#define DC_STR_FORWARDED 97
/// "Quota exceeding, already %1$s%% used."
///
/// Used as device message text.
///
/// `%1$s` will be replaced by the percentage used
/// @deprecated 2026-04-25
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
/// "Multi Device Synchronization"
+14 -49
View File
@@ -60,7 +60,6 @@ use self::string::*;
// - finally, this behaviour matches the old core-c API and UIs already depend on it
const DC_GCM_ADDDAYMARKER: u32 = 0x01;
const DC_GCM_INFO_ONLY: u32 = 0x02;
// dc_context_t
@@ -276,7 +275,7 @@ pub unsafe extern "C" fn dc_get_config(
.strdup()
} else {
match config::Config::from_str(&key)
.with_context(|| format!("Invalid key {:?}", &key))
.with_context(|| format!("Invalid key {key:?}"))
.log_err(ctx)
{
Ok(key) => ctx
@@ -1338,17 +1337,13 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
}
let ctx = &*context;
let info_only = (flags & DC_GCM_INFO_ONLY) != 0;
let add_daymarker = (flags & DC_GCM_ADDDAYMARKER) != 0;
block_on(async move {
Box::into_raw(Box::new(
chat::get_chat_msgs_ex(
ctx,
ChatId::new(chat_id),
MessageListOptions {
info_only,
add_daymarker,
},
MessageListOptions { add_daymarker },
)
.await
.unwrap_or_log_default(ctx, "failed to get chat msgs")
@@ -2546,7 +2541,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
}
let ctx = &*context;
block_on(location::send_locations_to_chat(
block_on(location::send_to_chat(
ctx,
ChatId::new(chat_id),
seconds as i64,
@@ -2566,14 +2561,14 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
return 0;
}
let ctx = &*context;
let chat_id = if chat_id == 0 {
None
if chat_id == 0 {
block_on(location::is_sending(ctx))
.unwrap_or_log_default(ctx, "Failed is_sending_locations()") as libc::c_int
} else {
Some(ChatId::new(chat_id))
};
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
block_on(location::is_sending_to_chat(ctx, ChatId::new(chat_id)))
.unwrap_or_log_default(ctx, "Failed is_sending_locations_to_chat()")
as libc::c_int
}
}
#[no_mangle]
@@ -2589,12 +2584,9 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(async move {
location::set(ctx, latitude, longitude, accuracy)
.await
.log_err(ctx)
.unwrap_or_default()
}) as libc::c_int
block_on(location::set(ctx, latitude, longitude, accuracy))
.log_err(ctx)
.unwrap_or_default() as libc::c_int
}
#[no_mangle]
@@ -2629,23 +2621,6 @@ pub unsafe extern "C" fn dc_get_locations(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_delete_all_locations()");
return;
}
let ctx = &*context;
block_on(async move {
location::delete_all(ctx)
.await
.context("Failed to delete locations")
.log_err(ctx)
.ok()
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
if payload.is_null() {
@@ -2826,7 +2801,7 @@ pub unsafe extern "C" fn dc_array_search_id(
// Returns 1 if location belongs to the track of the user,
// 0 if location was reported independently.
#[no_mangle]
pub unsafe fn dc_array_is_independent(
pub unsafe extern "C" fn dc_array_is_independent(
array: *const dc_array_t,
index: libc::size_t,
) -> libc::c_int {
@@ -4054,16 +4029,6 @@ pub unsafe extern "C" fn dc_msg_get_saved_msg_id(msg: *const dc_msg_t) -> u32 {
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_force_plaintext()");
return;
}
let ffi_msg = &mut *msg;
ffi_msg.message.force_plaintext();
}
// dc_contact_t
/// FFI struct for [dc_contact_t]
-2
View File
@@ -230,7 +230,6 @@ pub enum LotState {
MsgInFresh = 10,
MsgInNoticed = 13,
MsgInSeen = 16,
MsgOutPreparing = 18,
MsgOutDraft = 19,
MsgOutPending = 20,
MsgOutFailed = 24,
@@ -246,7 +245,6 @@ impl From<MessageState> for LotState {
InFresh => LotState::MsgInFresh,
InNoticed => LotState::MsgInNoticed,
InSeen => LotState::MsgInSeen,
OutPreparing => LotState::MsgOutPreparing,
OutDraft => LotState::MsgOutDraft,
OutPending => LotState::MsgOutPending,
OutFailed => LotState::MsgOutFailed,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.48.0-dev"
version = "2.50.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
+106 -37
View File
@@ -678,7 +678,7 @@ impl CommandApi {
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
}
/// Gets messages to be processed by the bot and returns their IDs.
/// (deprecated) Gets messages to be processed by the bot and returns their IDs.
///
/// Only messages with database ID higher than `last_msg_id` config value
/// are returned. After processing the messages, the bot should
@@ -686,6 +686,13 @@ impl CommandApi {
/// or manually updating the value to avoid getting already
/// processed messages.
///
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
/// even if it is not fully downloaded yet.
/// The bot needs to wait for the message to be fully downloaded.
/// Since this is usually not the desired behavior,
/// bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
/// event for getting notified about new messages.
///
/// [`markseen_msgs`]: Self::markseen_msgs
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
@@ -698,7 +705,7 @@ impl CommandApi {
Ok(msg_ids)
}
/// Waits for messages to be processed by the bot and returns their IDs.
/// (deprecated) Waits for messages to be processed by the bot and returns their IDs.
///
/// This function is similar to [`get_next_msgs`],
/// but waits for internal new message notification before returning.
@@ -709,6 +716,13 @@ impl CommandApi {
/// To shutdown the bot, stopping I/O can be used to interrupt
/// pending or next `wait_next_msgs` call.
///
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
/// even if it is not fully downloaded yet.
/// The bot needs to wait for the message to be fully downloaded.
/// Since this is usually not the desired behavior,
/// bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
/// event for getting notified about new messages.
///
/// [`get_next_msgs`]: Self::get_next_msgs
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
@@ -721,10 +735,19 @@ impl CommandApi {
Ok(msg_ids)
}
/// Estimate the number of messages that will be deleted
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
/// Estimates the number of messages that will be deleted
/// by the `set_config()`-option `delete_device_after`.
///
/// This is typically used to show the estimated impact to the user
/// before actually enabling deletion of old messages.
///
/// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
///
/// Parameters:
/// - `from_server`: Deprecated, pass `false` here
/// - `seconds`: Count messages older than the given number of seconds.
///
/// Returns the number of messages that are older than the given number of seconds.
async fn estimate_auto_deletion_count(
&self,
account_id: u32,
@@ -1352,8 +1375,22 @@ impl CommandApi {
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
}
/// Returns all messages of a particular chat.
/// Get all message IDs belonging to a chat.
///
/// The list is already sorted and starts with the oldest message.
/// Clients should not try to re-sort the list as this would be an expensive action
/// and would result in inconsistencies between clients.
/// Note that the messages are not necessarily sorted by their ID or by their displayed timestamp;
/// UIs need to handle both the case of descending message IDs
/// and of decreasing timestamps.
///
/// Optionally, 'daymarkers' added to the ID array may help to
/// implement virtual lists.
///
/// Parameters:
///
/// * chat_id The chat ID of which the messages IDs should be queried.
/// * _info_only: Deprecated, pass `false` here.
/// * `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.
@@ -1361,17 +1398,14 @@ impl CommandApi {
&self,
account_id: u32,
chat_id: u32,
info_only: bool,
_info_only: bool,
add_daymarker: bool,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions {
info_only,
add_daymarker,
},
MessageListOptions { add_daymarker },
)
.await?;
Ok(msg
@@ -1403,21 +1437,24 @@ impl CommandApi {
}
}
/// Get all messages belonging to a chat.
///
/// Similar to `get_message_ids` / `getMessageIds`,
/// see that function for details.
/// The difference is that this function here returns a list of `MessageListItem`,
/// which is an enum of a message or a daymarker.
async fn get_message_list_items(
&self,
account_id: u32,
chat_id: u32,
info_only: bool,
_info_only: bool,
add_daymarker: bool,
) -> Result<Vec<JsonrpcMessageListItem>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions {
info_only,
add_daymarker,
},
MessageListOptions { add_daymarker },
)
.await?;
Ok(msg
@@ -1854,20 +1891,6 @@ impl CommandApi {
deltachat::contact::make_vcard(&ctx, &contacts).await
}
/// Sets vCard containing the given contacts to the message draft.
async fn set_draft_vcard(
&self,
account_id: u32,
msg_id: u32,
contacts: Vec<u32>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
msg.make_vcard(&ctx, &contacts).await?;
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
}
// ---------------------------------------------
// chat
// ---------------------------------------------
@@ -2092,6 +2115,21 @@ impl CommandApi {
// locations
// ---------------------------------------------
/// Sets current location.
///
/// Returns true if location streaming is currently
/// enabled and locations should be updated.
///
/// Location is represented as latitude and longitude in degrees
/// and horizontal accuracy in meters.
async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
self.accounts
.read()
.await
.set_location(latitude, longitude, accuracy)
.await
}
async fn get_locations(
&self,
account_id: u32,
@@ -2114,6 +2152,39 @@ impl CommandApi {
Ok(locations.into_iter().map(|l| l.into()).collect())
}
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
///
/// Pass 0 as the number of seconds to disable location streaming in the chat.
async fn send_locations_to_chat(
&self,
account_id: u32,
chat_id: u32,
seconds: i64,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
location::send_to_chat(&ctx, chat_id, seconds).await?;
Ok(())
}
/// Returns whether any chat is sending locations.
async fn is_sending_locations(&self, account_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
location::is_sending(&ctx).await
}
/// Returns whether `chat_id` is sending locations.
async fn is_sending_locations_to_chat(&self, account_id: u32, chat_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
location::is_sending_to_chat(&ctx, chat_id).await
}
/// Stops sending locations to all chats.
async fn stop_sending_locations(&self) -> Result<()> {
self.accounts.read().await.stop_sending_locations().await
}
// ---------------------------------------------
// webxdc
// ---------------------------------------------
@@ -2345,6 +2416,7 @@ impl CommandApi {
chat::resend_msgs(&ctx, &message_ids).await
}
/// @deprecated as of 2026-04; use `send_msg` with `Viewtype::Sticker` instead.
async fn send_sticker(
&self,
account_id: u32,
@@ -2356,19 +2428,16 @@ impl CommandApi {
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file_and_deduplicate(&ctx, Path::new(&sticker_path), None, None)?;
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
msg.force_sticker();
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
/// Send a reaction to message.
/// Sends a reaction to message.
///
/// Reaction is a string of emojis separated by spaces. Reaction to a
/// single message can be sent multiple times. The last reaction
/// received overrides all previously received reactions. It is
/// possible to remove all reactions by sending an empty string.
/// A reaction is a string that represents an emoji.
/// You can call this function again to change the emoji;
/// the last sent reaction overrides all previously sent reactions.
/// It is possible to remove the reaction by sending an empty string.
async fn send_reaction(
&self,
account_id: u32,
+10 -2
View File
@@ -33,6 +33,12 @@ pub struct EnteredLoginParam {
/// Imap server port.
pub imap_port: Option<u16>,
/// IMAP server folder.
///
/// Defaults to "INBOX" if not set.
/// Should not be an empty string.
pub imap_folder: Option<String>,
/// Imap socket security.
pub imap_security: Option<Socket>,
@@ -85,6 +91,7 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
password: param.imap.password,
imap_server: param.imap.server.into_option(),
imap_port: param.imap.port.into_option(),
imap_folder: param.imap.folder.into_option(),
imap_security: imap_security.into_option(),
imap_user: param.imap.user.into_option(),
smtp_server: param.smtp.server.into_option(),
@@ -104,14 +111,15 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
fn try_from(param: EnteredLoginParam) -> Result<Self> {
Ok(Self {
addr: param.addr,
imap: dc::EnteredServerLoginParam {
imap: dc::EnteredImapLoginParam {
server: param.imap_server.unwrap_or_default(),
port: param.imap_port.unwrap_or_default(),
folder: param.imap_folder.unwrap_or_default(),
security: param.imap_security.unwrap_or_default().into(),
user: param.imap_user.unwrap_or_default(),
password: param.password,
},
smtp: dc::EnteredServerLoginParam {
smtp: dc::EnteredSmtpLoginParam {
server: param.smtp_server.unwrap_or_default(),
port: param.smtp_port.unwrap_or_default(),
security: param.smtp_security.unwrap_or_default().into(),
@@ -287,8 +287,6 @@ pub enum MessageViewtype {
Gif,
/// Message containing a sticker, similar to image.
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
///
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
+9 -9
View File
@@ -238,7 +238,7 @@ impl From<Qr> for QrObject {
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
@@ -257,7 +257,7 @@ impl From<Qr> for QrObject {
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::AskVerifyGroup {
grpname,
grpid,
@@ -278,7 +278,7 @@ impl From<Qr> for QrObject {
is_v3,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::AskJoinBroadcast {
name,
grpid,
@@ -321,7 +321,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
@@ -338,7 +338,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
@@ -357,7 +357,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::WithdrawJoinBroadcast {
name,
grpid,
@@ -374,7 +374,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
@@ -391,7 +391,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
@@ -410,7 +410,7 @@ impl From<Qr> for QrObject {
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
let fingerprint = fingerprint.human_readable();
QrObject::ReviveJoinBroadcast {
name,
grpid,
+9 -18
View File
@@ -24,6 +24,8 @@ pub struct JsonrpcReaction {
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JsonrpcReactions {
/// Map from a contact to it's reaction to message.
/// There is only a single reaction per contact,
/// but this contains a list of reactions for historical reasons.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count, sorted in descending order.
reactions: Vec<JsonrpcReaction>,
@@ -31,27 +33,16 @@ pub struct JsonrpcReactions {
impl From<Reactions> for JsonrpcReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id);
if reaction.is_empty() {
continue;
}
let emojis: Vec<String> = reaction
.emojis()
.into_iter()
.map(|emoji| emoji.to_owned())
.collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
}
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
.iter()
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
.collect();
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());
let mut reactions_v = Vec::new();
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
let is_from_self = if let Some(self_reactions) = self_reactions {
self_reactions.contains(&emoji)
let is_from_self = if let Some(self_reaction) = self_reaction {
self_reaction.contains(&emoji)
} else {
false
};
@@ -37,6 +37,10 @@ pub struct WebxdcMessageInfo {
internet_access: bool,
/// Address to be used for `window.webxdc.selfAddr` in JS land.
self_addr: String,
/// Define if the local user is the one who initially shared the webxdc application in the chat.
is_app_sender: bool,
/// Define if the app runs in a broadcasting context.
is_broadcast: bool,
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
/// Should be exposed to `window.sendUpdateInterval` in JS land.
send_update_interval: usize,
@@ -60,6 +64,8 @@ impl WebxdcMessageInfo {
request_integration: _,
internet_access,
self_addr,
is_app_sender,
is_broadcast,
send_update_interval,
send_update_max_size,
} = message.get_webxdc_info(context).await?;
@@ -72,6 +78,8 @@ impl WebxdcMessageInfo {
source_code_url: maybe_empty_string_to_option(source_code_url),
internet_access,
self_addr,
is_app_sender,
is_broadcast,
send_update_interval,
send_update_max_size,
})
+1 -1
View File
@@ -85,7 +85,7 @@ mod tests {
assert_eq!(result, response.to_owned());
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;
+1 -1
View File
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.48.0-dev"
"version": "2.50.0-dev"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.48.0-dev"
version = "2.50.0-dev"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
+9 -23
View File
@@ -122,7 +122,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name);
let path_plus_name = format!("{real_spec}/{name}");
println!("Import: {path_plus_name}");
if poke_eml_file(context, Path::new(&path_plus_name))
.await
@@ -133,11 +133,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
}
} else {
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
eprintln!("Import: Cannot open directory {real_spec:?}.");
return false;
}
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
println!("Import: {read_cnt} items read from {real_spec:?}.");
if read_cnt > 0 {
context.emit_msgs_changed_without_ids();
}
@@ -179,7 +179,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
if msg.has_location() { "📍" } else { "" },
&contact_name,
contact_name,
contact_id,
msgtext,
if msg.has_html() { "[HAS-HTML]️" } else { "" },
@@ -221,7 +221,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
},
statestr,
downloadstate,
&temp2,
temp2,
);
}
@@ -345,7 +345,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chatinfo\n\
sendlocations <seconds>\n\
setlocation <lat> <lng>\n\
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
send-sync <text>\n\
@@ -562,7 +561,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.map_or_else(String::new, |prefix| format!("{prefix}: ")),
summary.text,
statestr,
&timestr,
timestr,
if chat.is_sending_locations() {
"📍"
} else {
@@ -574,7 +573,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
}
}
if location::is_sending_locations_to_chat(&context, None).await? {
if location::is_sending(&context).await? {
println!("Location streaming enabled.");
}
println!("{cnt} chats");
@@ -623,7 +622,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
&context,
sel_chat.get_id(),
chat::MessageListOptions {
info_only: false,
add_daymarker: true,
},
)
@@ -782,11 +780,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!(
"Location streaming: {}",
location::is_sending_locations_to_chat(
&context,
Some(sel_chat.as_ref().unwrap().get_id())
)
.await?,
location::is_sending_to_chat(&context, sel_chat.as_ref().unwrap().get_id()).await?,
);
}
"getlocations" => {
@@ -826,12 +820,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "No timeout given.");
let seconds = arg1.parse()?;
location::send_locations_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
seconds,
)
.await?;
location::send_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), seconds).await?;
println!(
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
sel_chat.as_ref().unwrap().get_id(),
@@ -853,9 +842,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Success, streaming can be stopped.");
}
}
"dellocations" => {
location::delete_all(&context).await?;
}
"send" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No message text given.");
+2 -3
View File
@@ -176,7 +176,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 40] = [
const CHAT_COMMANDS: [&str; 39] = [
"listchats",
"listarchived",
"start-realtime",
@@ -194,7 +194,6 @@ const CHAT_COMMANDS: [&str; 40] = [
"chatinfo",
"sendlocations",
"setlocation",
"dellocations",
"getlocations",
"send",
"send-sync",
@@ -433,7 +432,7 @@ async fn handle_cmd(
{
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
} else {
println!("OAuth2 not available for {}.", &addr);
println!("OAuth2 not available for {addr}.");
}
} else {
println!("oauth2: set addr first.");
+1 -1
View File
@@ -29,7 +29,7 @@ $ pip install .
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Install tox `pip install -U tox`
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
3. Run `CHATMAIL_DOMAIN=ci-chatmail.testrun.org PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
@@ -13,7 +13,7 @@ def main():
with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
logging.info(f"Running deltachat core {system_info['deltachat_core_version']}")
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
@@ -21,36 +21,30 @@ def main():
account.set_config("bot", "1")
if not account.is_configured():
logging.info("Account is not configured, configuring")
account.set_config("addr", sys.argv[1])
account.set_config("mail_pw", sys.argv[2])
account.configure()
account.add_or_update_transport({"addr": sys.argv[1], "password": sys.argv[2]})
logging.info("Configured")
else:
logging.info("Account is already configured")
deltachat.start_io()
def process_messages():
for message in account.get_next_messages():
qr = account.get_qr_code()
logging.info(f"Invite link: {qr}")
while True:
event = account.wait_for_event()
if event.kind == EventType.INFO:
logging.info(event["msg"])
elif event.kind == EventType.WARNING:
logging.warning(event["msg"])
elif event.kind == EventType.ERROR:
logging.error(event["msg"])
elif event.kind == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
message = account.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
snapshot.chat.send_text(snapshot.text)
snapshot.message.mark_seen()
# Process old messages.
process_messages()
while True:
event = account.wait_for_event()
if event["kind"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["kind"] == EventType.WARNING:
logging.warning("%s", event["msg"])
elif event["kind"] == EventType.ERROR:
logging.error("%s", event["msg"])
elif event["kind"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
process_messages()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.48.0-dev"
version = "2.50.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
@@ -5,7 +5,6 @@ from __future__ import annotations
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from warnings import warn
from ._utils import AttrDict, futuremethod
from .chat import Chat
@@ -392,8 +391,7 @@ class Account:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
to process oldest messages first.
If you are writing a bot, process "incoming message" events instead.
"""
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
@@ -405,7 +403,15 @@ class Account:
@futuremethod
def wait_next_messages(self) -> list[Message]:
"""Wait for new messages and return a list of them."""
"""(deprecated) Wait for new messages and return a list of them. Meant for bots.
Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
even if it is not fully downloaded yet.
The bot needs to wait for the message to be fully downloaded.
Since this is usually not the desired behavior,
bots should instead use the `EventType.INCOMING_MSG`
event for getting notified about new messages.
"""
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
@@ -455,16 +461,6 @@ class Account:
"""Wait for reaction change event."""
return self.wait_for_event(EventType.REACTIONS_CHANGED)
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning,
stacklevel=2,
)
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def export_backup(self, path, passphrase: str = "") -> None:
"""Export backup."""
self._rpc.export_backup(self.id, str(path), passphrase)
@@ -487,3 +483,7 @@ class Account:
"""Return ICE servers for WebRTC configuration."""
ice_servers_json = self._rpc.ice_servers(self.id)
return json.loads(ice_servers_json)
def is_sending_locations(self) -> bool:
"""Return True if sending locations to any chat."""
return self._rpc.is_sending_locations(self.id)
@@ -164,7 +164,7 @@ class Chat:
return Message(self.account, msg_id)
def send_sticker(self, path: str) -> Message:
"""Send an sticker and return the resulting Message instance."""
"""Deprecated as of 2026-04; use `send_message` with `Viewtype.STICKER` instead."""
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
@@ -206,9 +206,9 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
def get_messages(self, add_daymarker: bool = False) -> list[Message]:
"""Get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
msgs = self._rpc.get_message_ids(self.account.id, self.id, False, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
def get_fresh_message_count(self) -> int:
@@ -277,6 +277,16 @@ class Chat:
"""Remove profile image of this chat."""
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
def send_locations(self, seconds) -> None:
"""Enable location streaming in the chat for the given number of seconds.
Pass 0 to disable location streaming."""
self._rpc.send_locations_to_chat(self.account.id, self.id, seconds)
def is_sending_locations(self) -> bool:
"""Return True if sending locations to this chat."""
return self._rpc.is_sending_locations_to_chat(self.account.id, self.id)
def get_locations(
self,
contact: Optional[Contact] = None,
@@ -190,7 +190,6 @@ class MessageState(IntEnum):
IN_FRESH = 10
IN_NOTICED = 13
IN_SEEN = 16
OUT_PREPARING = 18
OUT_DRAFT = 19
OUT_PENDING = 20
OUT_FAILED = 24
@@ -59,3 +59,11 @@ class DeltaChat:
def set_translations(self, translations: dict[str, str]) -> None:
"""Set stock translation strings."""
self.rpc.set_stock_strings(translations)
def set_location(self, latitude, longitude, accuracy) -> bool:
"""Set location, return True if location streaming should continue."""
return self.rpc.set_location(latitude, longitude, accuracy)
def stop_sending_locations(self) -> None:
"""Stop sending locations to all chats."""
return self.rpc.stop_sending_locations()
@@ -25,7 +25,14 @@ class Message:
return self.account._rpc
def send_reaction(self, *reaction: str) -> "Message":
"""Send a reaction to this message."""
"""
Sends a reaction to message.
A reaction is a string that represents an emoji.
You can call this function again to change the emoji;
the last sent reaction overrides all previously sent reactions.
It is possible to remove the reaction by sending an empty string.
"""
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
return Message(self.account, msg_id)
+23 -108
View File
@@ -1,104 +1,29 @@
import logging
import re
import time
import pytest
from imap_tools import AND, U
from deltachat_rpc_client import Contact, EventType, Message
from deltachat_rpc_client import EventType
def test_move_works(acfactory, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("DeltaChat")
ac2.set_config("mvbox_move", "1")
ac2.bring_online()
chat = ac1.create_chat(ac2)
chat.send_text("message1")
# Message is moved to the movebox
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
# Message is downloaded
msg = ac2.wait_for_incoming_msg().get_snapshot()
assert msg.text == "message1"
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
def test_moved_markseen(acfactory, direct_imap, log):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1 = acfactory.get_online_account()
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("DeltaChat")
ac2.set_config("mvbox_move", "1")
assert ac2.is_configured()
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
logging.info("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
msg1.wait_until_delivered()
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")
logging.info("receiving messages by ac2")
ac2.start_io()
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_snapshot().text == msg1.get_snapshot().text
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_move_works_on_self_sent(acfactory, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
# Create and enable movebox.
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.create_folder("DeltaChat")
ac1.set_config("mvbox_move", "1")
# Make sure that messages are not immediately auto-deleted on the server:
ac1.set_config("bcc_self", "1")
ac1.bring_online()
ac2.set_config("bcc_self", "1")
chat = ac1.create_chat(ac2)
chat.send_text("message1")
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
chat.send_text("message2")
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
chat.send_text("message3")
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
def test_moved_markseen(acfactory, direct_imap):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1, ac2 = acfactory.get_online_accounts(2)
log.section("ac2: creating DeltaChat folder")
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("DeltaChat")
ac2.set_config("mvbox_move", "1")
ac2.set_config("delete_server_after", "0")
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
ac2.bring_online()
ac2.stop_io()
@@ -108,6 +33,7 @@ def test_moved_markseen(acfactory, direct_imap):
idle2.wait_for_new_message()
# Emulate moving of the message to DeltaChat folder by Sieve rule.
log.section("ac2: moving message into DeltaChat folder")
ac2_direct_imap.conn.move(["*"], "DeltaChat")
ac2_direct_imap.select_folder("DeltaChat")
assert len(list(ac2_direct_imap.conn.fetch("*", mark_seen=False))) == 1
@@ -131,29 +57,18 @@ def test_moved_markseen(acfactory, direct_imap):
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
@pytest.mark.parametrize("mvbox_move", [True, False])
def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
def test_markseen_message_and_mdn(acfactory, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
for ac in ac1, ac2:
ac.set_config("delete_server_after", "0")
if mvbox_move:
ac_direct_imap = direct_imap(ac)
ac_direct_imap.create_folder("DeltaChat")
ac.set_config("mvbox_move", "1")
ac.bring_online()
# Do not send BCC to self, we only want to test MDN on ac1.
ac1.set_config("bcc_self", "0")
# Make sure that messages are not immediately auto-deleted on the server:
ac1.set_config("bcc_self", "1")
ac2.set_config("bcc_self", "1")
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2.wait_for_incoming_msg()
msg.mark_seen()
if mvbox_move:
rex = re.compile("Marked messages [0-9]+ in folder DeltaChat as seen.")
else:
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
for ac in ac1, ac2:
while True:
@@ -161,24 +76,24 @@ def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
if event.kind == EventType.INFO and rex.search(event.msg):
break
folder = "mvbox" if mvbox_move else "inbox"
ac1_direct_imap = direct_imap(ac1)
ac2_direct_imap = direct_imap(ac2)
ac1_direct_imap.select_config_folder(folder)
ac2_direct_imap.select_config_folder(folder)
ac1_direct_imap.select_folder("INBOX")
ac2_direct_imap.select_folder("INBOX")
# Check that the mdn is marked as seen
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
# Check original message is marked as seen
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
# Check that the mdn and original message is marked as seen
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
def test_trash_multiple_messages(acfactory, direct_imap, log):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.stop_io()
ac2.set_config("delete_server_after", "0")
# Make sure that messages are not immediately auto-deleted on the server:
ac2.set_config("bcc_self", "1")
ac2.set_config("sync_msgs", "0")
ac2.start_io()
@@ -0,0 +1,32 @@
def test_set_location(dc, acfactory) -> None:
# Try setting location without any accounts.
assert not dc.set_location(1.0, 2.0, 0.1)
# Create one account that does not stream,
# set location.
acfactory.new_configured_account()
assert not dc.set_location(3.0, 4.0, 0.1)
def test_send_locations_to_chat(dc, acfactory):
alice, bob = acfactory.get_online_accounts(2)
assert not alice.is_sending_locations()
alice_chat_bob = alice.create_chat(bob)
assert not alice_chat_bob.is_sending_locations()
# Test starting and stopping location streaming in a chat.
alice_chat_bob.send_locations(3600)
assert alice.is_sending_locations()
assert alice_chat_bob.is_sending_locations()
alice_chat_bob.send_locations(0)
assert not alice.is_sending_locations()
assert not alice_chat_bob.is_sending_locations()
# Test stop_sending_locations() for all accounts and chats.
alice_chat_bob.send_locations(3600)
assert alice.is_sending_locations()
assert alice_chat_bob.is_sending_locations()
dc.stop_sending_locations()
assert not alice.is_sending_locations()
assert not alice_chat_bob.is_sending_locations()
+4 -14
View File
@@ -4,39 +4,29 @@ from deltachat_rpc_client import EventType
from deltachat_rpc_client.const import MessageState
def test_bcc_self_delete_server_after_defaults(acfactory):
"""Test default values for bcc_self and delete_server_after."""
def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
ac = acfactory.get_online_account()
# Initially after getting online
# the setting bcc_self is set to 0 because there is only one device
# and delete_server_after is "1", meaning immediate deletion.
assert ac.get_config("bcc_self") == "0"
assert ac.get_config("delete_server_after") == "1"
# Setup a second device.
ac_clone = ac.clone()
ac_clone.bring_online()
# Second device setup
# enables bcc_self and changes default delete_server_after.
# Second device setup enables bcc_self.
assert ac.get_config("bcc_self") == "1"
assert ac.get_config("delete_server_after") == "0"
assert ac_clone.get_config("bcc_self") == "1"
assert ac_clone.get_config("delete_server_after") == "0"
# Manually disabling bcc_self
# also restores the default for delete_server_after.
# Test manually disabling bcc_self
ac.set_config("bcc_self", "0")
assert ac.get_config("bcc_self") == "0"
assert ac.get_config("delete_server_after") == "1"
# Cloning the account again enables bcc_self
# Cloning the account again enables bcc_self again
# even though it was manually disabled.
ac_clone = ac.clone()
assert ac.get_config("bcc_self") == "1"
assert ac.get_config("delete_server_after") == "0"
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
@@ -9,12 +9,6 @@ def test_add_second_address(acfactory) -> None:
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
# When the first transport is created,
# mvbox_move and only_fetch_mvbox should be disabled.
assert account.get_config("mvbox_move") == "0"
assert account.get_config("only_fetch_mvbox") == "0"
assert account.get_config("show_emails") == "2"
qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 2
@@ -32,44 +26,6 @@ def test_add_second_address(acfactory) -> None:
account.delete_transport(second_addr)
assert len(account.list_transports()) == 2
# Enabling mvbox_move or only_fetch_mvbox
# is not allowed when multi-transport is enabled.
for option in ["mvbox_move", "only_fetch_mvbox"]:
with pytest.raises(JsonRpcError):
account.set_config(option, "1")
# show_emails does not matter for multi-relay, can be set to anything
account.set_config("show_emails", "0")
@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
def test_no_second_transport_with_mvbox(acfactory, key) -> None:
"""Test that second transport cannot be configured if mvbox is used."""
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
assert account.get_config("mvbox_move") == "0"
assert account.get_config("only_fetch_mvbox") == "0"
qr = acfactory.get_account_qr()
account.set_config(key, "1")
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
def test_second_transport_without_classic_emails(acfactory) -> None:
"""Test that second transport can be configured if classic emails are not fetched."""
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
assert account.get_config("show_emails") == "2"
qr = acfactory.get_account_qr()
account.set_config("show_emails", "0")
account.add_transport_from_qr(qr)
def test_change_address(acfactory) -> None:
"""Test Alice configuring a second transport and setting it as a primary one."""
@@ -147,44 +103,13 @@ def test_download_on_demand(acfactory) -> None:
assert msg.get_snapshot().download_state == dstate
@pytest.mark.parametrize("is_chatmail", ["0", "1"])
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
Disabling mvbox_move is required to be able to setup a second transport.
"""
account = acfactory.get_unconfigured_account()
account.set_config("fix_is_chatmail", "1")
account.set_config("is_chatmail", is_chatmail)
# The default value when the setting is unset is "1".
# This is not changed for compatibility with old databases
# imported from backups.
assert account.get_config("mvbox_move") == "1"
qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
# Once the first transport is set up,
# mvbox_move is disabled.
assert account.get_config("mvbox_move") == "0"
assert account.get_config("is_chatmail") == is_chatmail
def test_reconfigure_transport(acfactory) -> None:
"""Test that reconfiguring the transport works
even if settings not supported for multi-transport
like mvbox_move are enabled."""
"""Test that reconfiguring the transport works."""
account = acfactory.get_online_account()
account.set_config("mvbox_move", "1")
[transport] = account.list_transports()
account.add_or_update_transport(transport)
# Reconfiguring the transport should not reset
# the settings as if when configuring the first transport.
assert account.get_config("mvbox_move") == "1"
def test_transport_synchronization(acfactory, log) -> None:
"""Test synchronization of transports between devices."""
+5 -3
View File
@@ -1047,6 +1047,7 @@ def test_no_old_msg_is_fresh(acfactory):
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
assert len(list(ac1.get_fresh_messages())) == 1
ac1_clone.wait_for_incoming_msg_event()
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
@@ -1231,11 +1232,12 @@ def test_leave_and_delete_group(acfactory, log):
def test_immediate_autodelete(acfactory, direct_imap, log):
"""
`bcc_self` is off by default,
so that messages are supposed to be immediately autodeleted
"""
ac1, ac2 = acfactory.get_online_accounts(2)
# "1" means delete immediately, while "0" means do not delete
ac2.set_config("delete_server_after", "1")
log.section("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
ac2.create_chat(ac1)
@@ -18,6 +18,8 @@ def test_webxdc(acfactory) -> None:
"sourceCodeUrl": None,
"summary": None,
"selfAddr": webxdc_info["selfAddr"],
"isAppSender": False,
"isBroadcast": False,
"sendUpdateInterval": 1000,
"sendUpdateMaxSize": 18874368,
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.48.0-dev"
version = "2.50.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
+1 -1
View File
@@ -18,7 +18,7 @@ import { startDeltaChat } from "@deltachat/stdio-rpc-server";
import { C } from "@deltachat/jsonrpc-client";
async function main() {
const dc = await startDeltaChat("deltachat-data");
const dc = startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
dc.close()
}
+9 -2
View File
@@ -15,7 +15,7 @@ export interface SearchOptions {
*/
export function getRPCServerPath(
options?: Partial<SearchOptions>
): Promise<string>;
): string;
@@ -33,8 +33,15 @@ export interface StartOptions {
* @param directory directory for accounts folder
* @param options
*/
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): Promise<DeltaChatOverJsonRpcServer>
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): DeltaChatOverJsonRpcServer
export class DeltaChatOverJsonRpc extends StdioDeltaChat {
constructor(
directory: string,
options?: Partial<SearchOptions & StartOptions>
);
readonly pathToServerBinary: string;
}
export namespace FnTypes {
export type getRPCServerPath = typeof getRPCServerPath
+48 -40
View File
@@ -1,6 +1,6 @@
//@ts-check
import { spawn } from "node:child_process";
import { stat } from "node:fs/promises";
import { statSync } from "node:fs";
import os from "node:os";
import process from "node:process";
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
@@ -36,7 +36,7 @@ function findRPCServerInNodeModules() {
}
/** @type {import("./index").FnTypes.getRPCServerPath} */
export async function getRPCServerPath(options = {}) {
export function getRPCServerPath(options = {}) {
const { takeVersionFromPATH, disableEnvPath } = {
takeVersionFromPATH: false,
disableEnvPath: false,
@@ -45,7 +45,7 @@ export async function getRPCServerPath(options = {}) {
// 1. check if it is set as env var
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
try {
if (!(await stat(process.env[ENV_VAR_NAME])).isFile()) {
if (!statSync(process.env[ENV_VAR_NAME]).isFile()) {
throw new Error(
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
);
@@ -68,41 +68,49 @@ export async function getRPCServerPath(options = {}) {
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
/** @type {import("./index").FnTypes.startDeltaChat} */
export async function startDeltaChat(directory, options = {}) {
const pathToServerBinary = await getRPCServerPath(options);
const server = spawn(pathToServerBinary, {
env: {
RUST_LOG: process.env.RUST_LOG,
DC_ACCOUNTS_PATH: directory,
},
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
});
server.on("error", (err) => {
throw new Error(FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err));
});
let shouldClose = false;
server.on("exit", () => {
if (shouldClose) {
return;
}
throw new Error("Server quit");
});
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
//@ts-expect-error
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
dc.close = () => {
shouldClose = true;
if (!server.kill()) {
console.log("server termination failed");
}
};
//@ts-expect-error
dc.pathToServerBinary = pathToServerBinary;
return dc;
export function startDeltaChat(directory, options = {}) {
return new DeltaChatOverJsonRpc(directory, options);
}
export class DeltaChatOverJsonRpc extends StdioDeltaChat {
/**
*
* @param {string} directory
* @param {Partial<import("./index").SearchOptions & import("./index").StartOptions>} options
*/
constructor(directory, options = {}) {
const pathToServerBinary = getRPCServerPath(options);
const server = spawn(pathToServerBinary, {
env: {
RUST_LOG: process.env.RUST_LOG,
DC_ACCOUNTS_PATH: directory,
},
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
});
server.on("error", (err) => {
throw new Error(
FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err)
);
});
let shouldClose = false;
server.on("exit", () => {
if (shouldClose) {
return;
}
throw new Error("Server quit");
});
super(server.stdin, server.stdout, true);
this.close = () => {
shouldClose = true;
if (!server.kill()) {
console.log("server termination failed");
}
};
this.pathToServerBinary = pathToServerBinary;
}
}
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.48.0-dev"
"version": "2.50.0-dev"
}
+27
View File
@@ -23,7 +23,32 @@ ignore = [
# it is a transitive dependency of iroh 0.35.0
# which depends on ^0.102.
# <https://rustsec.org/advisories/RUSTSEC-2026-0049>
# <https://rustsec.org/advisories/RUSTSEC-2026-0098>
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
"RUSTSEC-2026-0049",
"RUSTSEC-2026-0098",
"RUSTSEC-2026-0099",
# Panic in CRL signature checks.
# We do not check CRL and cannot update rustls-webpki 0.102.8
# which is a dependency of iroh 0.35.0.
# <https://rustsec.org/advisories/RUSTSEC-2026-0104>
"RUSTSEC-2026-0104",
# hickory-proto 0.25.2 unbounded loop in DNSSEC code.
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
# <https://rustsec.org/advisories/RUSTSEC-2026-0118>
"RUSTSEC-2026-0118",
# hickory-proto 0.25.2 quadratic complexity issue.
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
# <https://rustsec.org/advisories/RUSTSEC-2026-0119>
"RUSTSEC-2026-0119",
# Timing side channel in ml-dsa dependency of rPGP.
# We enable PQC for encryption rather than signatures.
# <https://rustsec.org/advisories/RUSTSEC-2025-0144>
"RUSTSEC-2025-0144",
]
[bans]
@@ -35,12 +60,14 @@ skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "bitflags", version = "1.3.2" },
{ name = "constant_time_eq", version = "0.3.1" },
{ name = "cpufeatures", version = "0.2.17" },
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "getrandom", version = "0.2.12" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "hybrid-array", version = "0.2.3" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "lru", version = "0.12.5" },
{ name = "netlink-packet-route", version = "0.17.1" },
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.48.0-dev"
version = "2.50.0-dev"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
-29
View File
@@ -271,15 +271,6 @@ class Chat:
sent out. This is the same object as was passed in, which
has been modified with the new state of the core.
"""
if msg.is_out_preparing():
assert msg.id != 0
# get a fresh copy of dc_msg, the core needs it
maybe_msg = Message.from_db(self.account, msg.id)
if maybe_msg is not None:
msg = maybe_msg
else:
raise ValueError("message does not exist")
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
@@ -333,26 +324,6 @@ class Chat:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
def send_prepared(self, message):
"""send a previously prepared message.
:param message: a :class:`Message` instance previously returned by
:meth:`prepare_file`.
:raises ValueError: if message can not be sent.
:returns: a :class:`deltachat.message.Message` instance as sent out.
"""
assert message.id != 0 and message.is_out_preparing()
# get a fresh copy of dc_msg, the core needs it
msg = Message.from_db(self.account, message.id)
# pass 0 as chat-id because core-docs say it's ok when out-preparing
sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
assert sent_id == msg.id
# modify message in place to avoid bad state for the caller
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
def set_draft(self, message):
"""set message as draft.
-9
View File
@@ -254,10 +254,6 @@ class Message:
"""Quote setter."""
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
def force_plaintext(self) -> None:
"""Force the message to be sent in plain text."""
lib.dc_msg_force_plaintext(self._dc_msg)
@property
def error(self) -> Optional[str]:
"""Error message."""
@@ -355,17 +351,12 @@ class Message:
def is_outgoing(self):
"""Return True if Message is outgoing."""
return lib.dc_msg_get_state(self._dc_msg) in (
const.DC_STATE_OUT_PREPARING,
const.DC_STATE_OUT_PENDING,
const.DC_STATE_OUT_FAILED,
const.DC_STATE_OUT_MDN_RCVD,
const.DC_STATE_OUT_DELIVERED,
)
def is_out_preparing(self):
"""Return True if Message is outgoing, but its file is being prepared."""
return self._msgstate == const.DC_STATE_OUT_PREPARING
def is_out_pending(self):
"""Return True if Message is outgoing, but is pending (no single checkmark)."""
return self._msgstate == const.DC_STATE_OUT_PENDING
-4
View File
@@ -433,7 +433,6 @@ class ACFactory:
if self.pytestconfig.getoption("--strict-tls"):
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
assert "addr" in configdict and "mail_pw" in configdict
return configdict
@@ -505,7 +504,6 @@ class ACFactory:
"addr": cloned_from.get_config("addr"),
"mail_pw": cloned_from.get_config("mail_pw"),
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
"smtp_certificate_checks": cloned_from.get_config("smtp_certificate_checks"),
}
configdict.update(kwargs)
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
@@ -522,9 +520,7 @@ class ACFactory:
ac = self.get_unconfigured_account()
assert "addr" in configdict and "mail_pw" in configdict, configdict
configdict.setdefault("bcc_self", False)
configdict.setdefault("mvbox_move", False)
configdict.setdefault("sync_msgs", False)
configdict.setdefault("delete_server_after", 0)
ac.update_config(configdict)
self._acsetup._account2config[ac] = configdict
self._preconfigure_key(ac)
+1 -67
View File
@@ -288,6 +288,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message")
chat2.accept()
msg_out = chat2.send_text("hello")
lp.sec("ac1: receiving message")
@@ -297,73 +298,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert msg_in.text == msg_out.text
def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
"""Test for the issue #4346:
- User is added to a verified group.
- First device of the user downloads "member added" from the group.
- First device removes "member added" from the server.
- Some new messages are sent to the group.
- Second device comes online, receives these new messages.
The result is an unverified group with unverified members.
- First device re-gossips Autocrypt keys to the group.
- Now the second device has all members and group verified.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
acfactory.remove_preconfigured_keys()
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
ac2.set_config("delete_server_after", "1")
ac2.set_config("gossip_period", "0") # Re-gossip in every message
acfactory.bring_accounts_online()
dir = tmp_path / "exportdir"
dir.mkdir()
ac2.export_self_keys(str(dir))
ac2_offl.import_self_keys(str(dir))
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello")
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
# Wait for "Member Me (<addr>) added by <addr>." message.
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.is_system_message()
lp.sec("ac2: waiting for 'member added' to be deleted on the server")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
lp.sec("ac1: sending 'hi' to the group")
ac2.set_config("delete_server_after", "0")
chat1.send_text("hi")
lp.sec("ac2_offl: going online, checking the 'hi' message")
ac2_offl.start_io()
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
assert not msg_in.is_system_message()
assert msg_in.text == "hi"
ac2_offl_ac1_contact = msg_in.get_sender_contact()
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
assert not ac2_offl_ac1_contact.is_verified()
chat2_offl = msg_in.chat
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
lp.sec("ac2_offl: receiving message")
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert not msg_in.is_system_message()
assert msg_in.text == "hi2"
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not ac2_offl_ac1_contact.is_verified()
def test_deleted_msgs_dont_reappear(acfactory):
ac1 = acfactory.new_online_configuring_account()
acfactory.bring_accounts_online()
+19 -1
View File
@@ -15,6 +15,9 @@ def test_basic_imap_api(acfactory, tmp_path):
ac1, ac2 = acfactory.get_online_accounts(2)
chat12 = acfactory.get_accepted_chat(ac1, ac2)
# Make sure that messages are not immediately auto-deleted on the server:
ac2.set_config("bcc_self", "1")
imap2 = ac2.direct_imap
with imap2.idle() as idle2:
@@ -162,6 +165,9 @@ def test_webxdc_message(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
# Make sure that messages are not immediately auto-deleted on the server:
ac2.set_config("bcc_self", "1")
lp.sec("ac1: prepare and send text message to ac2")
msg1 = chat.send_text("message0")
assert not msg1.is_webxdc()
@@ -362,6 +368,10 @@ def test_send_and_receive_message_markseen(acfactory, lp):
# make DC's life harder wrt to encodings
ac1.set_config("displayname", "ä name")
# Make sure that messages are not immediately auto-deleted on the server:
ac1.set_config("bcc_self", "1")
ac2.set_config("bcc_self", "1")
# clear any fresh device messages
ac1.get_device_chat().mark_noticed()
ac2.get_device_chat().mark_noticed()
@@ -506,9 +516,15 @@ def test_mdn_asymmetric(acfactory, lp):
ac1.set_config("mdns_enabled", "1")
ac2.set_config("mdns_enabled", "1")
# Make sure that the mdn is not immediately auto-deleted on the server:
ac1.set_config("bcc_self", "1")
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
# Wait for the message to be marked as seen on IMAP.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
lp.sec("disable ac1 MDNs")
@@ -525,7 +541,7 @@ def test_mdn_asymmetric(acfactory, lp):
lp.sec("ac1: waiting for incoming activity")
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
# Wait for the message to be marked as seen on IMAP.
# Wait for the mdn to be marked as seen on IMAP.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
# MDN is received even though MDNs are already disabled
@@ -1073,6 +1089,8 @@ def test_send_receive_locations(acfactory, lp):
def test_delete_multiple_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
# Make sure that messages are not immediately auto-deleted on the server:
ac2.set_config("bcc_self", "1")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: sending seven messages")
+10 -10
View File
@@ -52,19 +52,19 @@ class TestOfflineAccountBasic:
def test_set_config_int_conversion(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
ac1.set_config("mvbox_move", False)
assert ac1.get_config("mvbox_move") == "0"
ac1.set_config("mvbox_move", True)
assert ac1.get_config("mvbox_move") == "1"
ac1.set_config("mvbox_move", 0)
assert ac1.get_config("mvbox_move") == "0"
ac1.set_config("mvbox_move", 1)
assert ac1.get_config("mvbox_move") == "1"
ac1.set_config("bcc_self", False)
assert ac1.get_config("bcc_self") == "0"
ac1.set_config("bcc_self", True)
assert ac1.get_config("bcc_self") == "1"
ac1.set_config("bcc_self", 0)
assert ac1.get_config("bcc_self") == "0"
ac1.set_config("bcc_self", 1)
assert ac1.get_config("bcc_self") == "1"
def test_update_config(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
ac1.update_config({"mvbox_move": False})
assert ac1.get_config("mvbox_move") == "0"
ac1.update_config({"bcc_self": True})
assert ac1.get_config("bcc_self") == "1"
def test_has_bccself(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
+1 -1
View File
@@ -1 +1 @@
2026-03-24
2026-04-13
-16
View File
@@ -48,19 +48,3 @@ the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type
This will **rsync** your current checkout to the remote build machine
(no need to commit before) and then run either rust or python tests.
# coredeps Dockerfile
`coredeps/Dockerfile` specifies an image that contains all
of Delta Chat's core dependencies. It is used to
build python wheels (binary packages for Python).
You can build the docker images yourself locally
to avoid the relatively large download:
cd scripts # where all CI things are
docker build -t deltachat/coredeps coredeps
Additionally, you can install qemu and build arm64 docker image on x86\_64 machine:
apt-get install qemu binfmt-support qemu-user-static
docker build -t deltachat/coredeps-arm64 --build-arg BASEIMAGE=quay.io/pypa/manylinux2014_aarch64 coredeps
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
#
# Script to check that current version ends with -dev.
# Meant to be run in CI to check that PRs are made against the -dev version.
# If the version is not -dev, it was forgotten to be updated
# after making a release.
from pathlib import Path
import tomllib
import sys
def main():
with Path("Cargo.toml").open("rb") as fp:
cargo_toml = tomllib.load(fp)
version = cargo_toml["package"]["version"]
if not version.endswith("-dev"):
print(f"Current version {version} does not end with -dev", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+7 -1
View File
@@ -1,3 +1,9 @@
#!/bin/sh
# Run clippy for all Rust code in the project.
cargo clippy --workspace --all-targets --all-features -- -D warnings
#
# To check, run
# scripts/clippy.sh
#
# To automatically fix warnings, run
# scripts/clippy.sh --fix --allow-dirty
cargo clippy --workspace --all-targets --all-features "$@" -- -D warnings
-26
View File
@@ -1,26 +0,0 @@
# Concourse CI pipeline
`docs_wheels.yml` is a pipeline for [Concourse CI](https://concourse-ci.org/)
that builds C documentation, Python documentation, Python wheels for `x86_64`
and `aarch64` and Python source packages, and uploads them.
To setup the pipeline run
```
fly -t <your-target> set-pipeline -c docs_wheels.yml -p docs_wheels -l secret.yml
```
where `secret.yml` contains the following secrets:
```
c.delta.chat:
private_key: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
devpi:
login: dc
password: ...
```
Secrets can be read from the password manager:
```
fly -t b1 set-pipeline -c docs_wheels.yml -p docs_wheels -l <(pass show delta/b1.delta.chat/secret.yml)
```
-305
View File
@@ -1,305 +0,0 @@
resources:
- name: deltachat-core-rust
type: git
icon: github
source:
branch: main
uri: https://github.com/chatmail/core.git
- name: deltachat-core-rust-release
type: git
icon: github
source:
branch: main
uri: https://github.com/chatmail/core.git
tag_filter: "v*"
jobs:
- name: python-x86_64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: concourse/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_x86_64
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
# Binary wheels
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload x86_64 wheels and source packages
- task: upload-wheels
config:
inputs:
- name: py-wheels
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install devpi
env/bin/devpi use https://m.devpi.net/dc/master
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
env/bin/devpi upload py-wheels/*manylinux201*
- name: python-aarch64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: concourse/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_aarch64
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload aarch64 wheels
- task: upload-wheels
config:
inputs:
- name: py-wheels
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install devpi
env/bin/devpi use https://m.devpi.net/dc/master
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
env/bin/devpi upload py-wheels/*manylinux201*
- name: python-musl-x86_64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: concourse/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_x86_64
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload musl x86_64 wheels
- task: upload-wheels
config:
inputs:
- name: py-wheels
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install devpi
env/bin/devpi use https://m.devpi.net/dc/master
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
env/bin/devpi upload py-wheels/*musllinux_1_1_x86_64*
- name: python-musl-aarch64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: concourse/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_aarch64
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload musl aarch64 wheels
- task: upload-wheels
config:
inputs:
- name: py-wheels
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install devpi
env/bin/devpi use https://m.devpi.net/dc/master
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
env/bin/devpi upload py-wheels/*musllinux_1_1_aarch64*
-9
View File
@@ -1,9 +0,0 @@
ARG BASEIMAGE=quay.io/pypa/manylinux2014_x86_64
#ARG BASEIMAGE=quay.io/pypa/musllinux_1_1_x86_64
#ARG BASEIMAGE=quay.io/pypa/manylinux2014_aarch64
FROM $BASEIMAGE
RUN pipx install tox
COPY install-rust.sh /scripts/
RUN /scripts/install-rust.sh
RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Install Rust
#
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.94.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$ARCH-unknown-linux-$LIBC"
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
+1 -1
View File
@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=996c4bc82be5a7404f70b185ff062da33bfa98d9
REV=2cba4b72f4c6e6417b83ba549aff7781be5f166c
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"
+35 -1
View File
@@ -8,6 +8,7 @@ use std::sync::Arc;
use anyhow::{Context as _, Result, bail, ensure};
use async_channel::{self, Receiver, Sender};
use futures::FutureExt as _;
use futures::future;
use futures_lite::FutureExt as _;
use serde::{Deserialize, Serialize};
use tokio::fs;
@@ -22,6 +23,7 @@ use tokio::time::{Duration, sleep};
use crate::context::{Context, ContextBuilder};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::location;
use crate::log::warn;
use crate::push::PushSubscriber;
use crate::stock_str::StockStrings;
@@ -536,6 +538,38 @@ impl Accounts {
self.push_subscriber.set_device_token(token).await;
Ok(())
}
/// Sets location for all accounts.
///
/// Returns true if location should still be streamed.
pub async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
let continue_streaming = future::try_join_all(self.accounts.iter().map(
|(account_id, account)| async move {
location::set(account, latitude, longitude, accuracy)
.await
.with_context(|| format!("Failed to set location for account {account_id}"))
},
))
.await?
.into_iter()
.any(|continue_streaming| continue_streaming);
Ok(continue_streaming)
}
/// Stops sending locations to all chats.
pub async fn stop_sending_locations(&self) -> Result<()> {
future::try_join_all(
self.accounts
.iter()
.map(|(account_id, account)| async move {
location::stop_sending(account).await.with_context(|| {
format!("Failed to stop sending locations for account {account_id}")
})
}),
)
.await?;
Ok(())
}
}
/// Configuration file name.
@@ -760,7 +794,7 @@ impl Config {
.with_push_subscriber(push_subscriber.clone())
.build()
.await
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
.with_context(|| format!("failed to create context from file {dbfile:?}"))?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
+12 -16
View File
@@ -4,9 +4,8 @@
use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use anyhow::{Context as _, Error, Result, bail};
use anyhow::{Context as _, Result, bail};
use crate::key::{DcKey, SignedPublicKey};
@@ -28,10 +27,8 @@ impl fmt::Display for EncryptPreference {
}
}
impl FromStr for EncryptPreference {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
impl EncryptPreference {
fn new(s: &str) -> Result<Self> {
match s {
"mutual" => Ok(EncryptPreference::Mutual),
"nopreference" => Ok(EncryptPreference::NoPreference),
@@ -85,10 +82,8 @@ impl fmt::Display for Aheader {
}
}
impl FromStr for Aheader {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
impl Aheader {
pub(crate) fn from_str(s: &str) -> Result<Self> {
let mut attributes: BTreeMap<String, String> = s
.split(';')
.filter_map(|a| {
@@ -116,7 +111,7 @@ impl FromStr for Aheader {
let prefer_encrypt = attributes
.remove("prefer-encrypt")
.and_then(|raw| raw.parse().ok())
.and_then(|raw| EncryptPreference::new(&raw).ok())
.unwrap_or_default();
let verified = attributes.remove("_verified").is_some();
@@ -144,8 +139,9 @@ mod tests {
#[test]
fn test_from_str() -> Result<()> {
let h: Aheader =
format!("addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}").parse()?;
let h = Aheader::from_str(&format!(
"addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}"
))?;
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
@@ -157,7 +153,7 @@ mod tests {
#[test]
fn test_from_str_reset() -> Result<()> {
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
let h: Aheader = raw.parse()?;
let h = Aheader::from_str(&raw)?;
assert_eq!(h.addr, "reset@example.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
@@ -167,7 +163,7 @@ mod tests {
#[test]
fn test_from_str_non_critical() -> Result<()> {
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={RAWKEY}");
let h: Aheader = raw.parse()?;
let h = Aheader::from_str(&raw)?;
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
@@ -177,7 +173,7 @@ mod tests {
#[test]
fn test_from_str_superflous_critical() {
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={RAWKEY}");
assert!(raw.parse::<Aheader>().is_err());
assert!(Aheader::from_str(&raw).is_err());
}
#[test]
-561
View File
@@ -1,561 +0,0 @@
//! Parsing and handling of the Authentication-Results header.
//! See the comment on [`handle_authres`] for more.
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fmt;
use std::sync::LazyLock;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
use crate::config::Config;
use crate::context::Context;
use crate::headerdef::HeaderDef;
/// `authres` is short for the Authentication-Results header, defined in
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
/// about whether DKIM and SPF passed.
///
/// To mitigate From forgery, we remember for each sending domain whether it is known
/// to have valid DKIM. If an email from such a domain comes with invalid DKIM,
/// we don't allow changing the autocrypt key.
///
/// See <https://github.com/deltachat/deltachat-core-rust/issues/3507>.
pub(crate) async fn handle_authres(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
) -> Result<DkimResults> {
let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
}
};
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
update_authservid_candidates(context, &authres).await?;
compute_dkim_results(context, authres).await
}
#[derive(Debug)]
pub(crate) struct DkimResults {
/// Whether DKIM passed for this particular e-mail.
pub dkim_passed: bool,
}
impl fmt::Display for DkimResults {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "DKIM Results: Passed={}", self.dkim_passed)?;
Ok(())
}
}
type AuthservId = String;
#[derive(Debug, PartialEq)]
enum DkimResult {
/// The header explicitly said that DKIM passed
Passed,
/// The header explicitly said that DKIM failed
Failed,
/// The header didn't say anything about DKIM; this might mean that it wasn't
/// checked, but it might also mean that it failed. This is because some providers
/// (e.g. ik.me, mail.ru, posteo.de) don't add `dkim=none` to their
/// Authentication-Results if there was no DKIM.
Nothing,
}
type ParsedAuthresHeaders = Vec<(AuthservId, DkimResult)>;
fn parse_authres_headers(
headers: &mailparse::headers::Headers<'_>,
from_domain: &str,
) -> ParsedAuthresHeaders {
let mut res = Vec::new();
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
let header_value = remove_comments(&header_value);
if let Some(mut authserv_id) = header_value.split(';').next() {
if authserv_id.contains(char::is_whitespace) || authserv_id.is_empty() {
// Outlook violates the RFC by not adding an authserv-id at all, which we notice
// because there is whitespace in the first identifier before the ';'.
// Authentication-Results-parsing still works securely because they remove incoming
// Authentication-Results headers.
// We just use an arbitrary authserv-id, it will work for Outlook, and in general,
// with providers not implementing the RFC correctly, someone can trick us
// into thinking that an incoming email is DKIM-correct, anyway.
// The most important thing here is that we have some valid `authserv_id`.
authserv_id = "invalidAuthservId";
}
let dkim_passed = parse_one_authres_header(&header_value, from_domain);
res.push((authserv_id.to_string(), dkim_passed));
}
}
res
}
/// The headers can contain comments that look like this:
/// ```text
/// Authentication-Results: (this is a comment) gmx.net; (another; comment) dkim=pass;
/// ```
fn remove_comments(header: &str) -> Cow<'_, str> {
// In Pomsky, this is:
// "(" Codepoint* lazy ")"
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
static RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
RE.replace_all(header, " ")
}
/// Parses a single Authentication-Results header, like:
///
/// ```text
/// Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
/// ```
fn parse_one_authres_header(header_value: &str, from_domain: &str) -> DkimResult {
if let Some((before_dkim_part, dkim_to_end)) = header_value.split_once("dkim=") {
// Check that the character right before `dkim=` is a space or a tab
// so that we wouldn't e.g. mistake `notdkim=pass` for `dkim=pass`
if before_dkim_part.ends_with(' ') || before_dkim_part.ends_with('\t') {
let dkim_part = dkim_to_end.split(';').next().unwrap_or_default();
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
if let Some(&"pass") = dkim_parts.first() {
// DKIM headers contain a header.d or header.i field
// that says which domain signed. We have to check ourselves
// that this is the same domain as in the From header.
let header_d: &str = &format!("header.d={}", &from_domain);
let header_i: &str = &format!("header.i=@{}", &from_domain);
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
// We have found a `dkim=pass` header!
return DkimResult::Passed;
}
} else {
// dkim=fail, dkim=none, ...
return DkimResult::Failed;
}
}
}
DkimResult::Nothing
}
/// ## About authserv-ids
///
/// After having checked DKIM, our email server adds an Authentication-Results header.
///
/// Now, an attacker could just add an Authentication-Results header that says dkim=pass
/// in order to make us think that DKIM was correct in their From-forged email.
///
/// In order to prevent this, each email server adds its authserv-id to the
/// Authentication-Results header, e.g. Testrun's authserv-id is `testrun.org`, Gmail's
/// is `mx.google.com`. When Testrun gets a mail delivered from outside, it will then
/// remove any Authentication-Results headers whose authserv-id is also `testrun.org`.
///
/// We need to somehow find out the authserv-id(s) of our email server, so that
/// we can use the Authentication-Results with the right authserv-id.
///
/// ## What this function does
///
/// When receiving an email, this function is called and updates the candidates for
/// our server's authserv-id, i.e. what we think our server's authserv-id is.
///
/// Usually, every incoming email has Authentication-Results with our server's
/// authserv-id, so, the intersection of the existing authserv-ids and the incoming
/// authserv-ids for our server's authserv-id is a good guess for our server's
/// authserv-id. When this intersection is empty, we assume that the authserv-id has
/// changed and start over with the new authserv-ids.
///
/// See [`handle_authres`].
async fn update_authservid_candidates(
context: &Context,
authres: &ParsedAuthresHeaders,
) -> Result<()> {
let mut new_ids: BTreeSet<&str> = authres
.iter()
.map(|(authserv_id, _dkim_passed)| authserv_id.as_str())
.collect();
if new_ids.is_empty() {
// The incoming message doesn't contain any authentication results, maybe it's a
// self-sent or a mailer-daemon message
return Ok(());
}
let old_config = context.get_config(Config::AuthservIdCandidates).await?;
let old_ids = parse_authservid_candidates_config(&old_config);
let intersection: BTreeSet<&str> = old_ids.intersection(&new_ids).copied().collect();
if !intersection.is_empty() {
new_ids = intersection;
}
// If there were no AuthservIdCandidates previously, just start with
// the ones from the incoming email
if old_ids != new_ids {
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
context
.set_config_internal(Config::AuthservIdCandidates, Some(&new_config))
.await?;
}
Ok(())
}
/// Use the parsed authres and the authservid candidates to compute whether DKIM passed
/// and whether a keychange should be allowed.
///
/// We track in the `sending_domains` table whether we get positive Authentication-Results
/// for mails from a contact (meaning that their provider properly authenticates against
/// our provider).
///
/// Once a contact is known to come with positive Authentication-Resutls (dkim: pass),
/// we don't accept Autocrypt key changes if they come with negative Authentication-Results.
async fn compute_dkim_results(
context: &Context,
mut authres: ParsedAuthresHeaders,
) -> Result<DkimResults> {
let mut dkim_passed = false;
let ids_config = context.get_config(Config::AuthservIdCandidates).await?;
let ids = parse_authservid_candidates_config(&ids_config);
// Remove all foreign authentication results
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
if authres.is_empty() {
// If the authentication results are empty, then our provider doesn't add them
// and an attacker could just add their own Authentication-Results, making us
// think that DKIM passed. So, in this case, we can as well assume that DKIM passed.
dkim_passed = true;
} else {
for (_authserv_id, current_dkim_passed) in authres {
match current_dkim_passed {
DkimResult::Passed => {
dkim_passed = true;
break;
}
DkimResult::Failed => {
dkim_passed = false;
break;
}
DkimResult::Nothing => {
// Continue looking for an Authentication-Results header
}
}
}
}
Ok(DkimResults { dkim_passed })
}
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
config
.as_deref()
.map(|c| c.split_whitespace().collect())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use tokio::fs;
use tokio::io::AsyncReadExt;
use super::*;
use crate::mimeparser;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
#[test]
fn test_remove_comments() {
let header = "Authentication-Results: mx3.messagingengine.com;
dkim=pass (1024-bit rsa key sha256) header.d=riseup.net;"
.to_string();
assert_eq!(
remove_comments(&header),
"Authentication-Results: mx3.messagingengine.com;
dkim=pass header.d=riseup.net;"
);
let header = ") aaa (".to_string();
assert_eq!(remove_comments(&header), ") aaa (");
let header = "((something weird) no comment".to_string();
assert_eq!(remove_comments(&header), " no comment");
let header = "🎉(🎉(🎉))🎉(".to_string();
assert_eq!(remove_comments(&header), "🎉 )🎉(");
// Comments are allowed to include whitespace
let header = "(com\n\t\r\nment) no comment (comment)".to_string();
assert_eq!(remove_comments(&header), " no comment ");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_authentication_results() -> Result<()> {
let t = TestContext::new().await;
t.configure_addr("alice@gmx.net").await;
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Passed),
("gmx.net".to_string(), DkimResult::Nothing)
]
);
let bytes = b"Authentication-Results: gmx.net; notdkim=pass header.i=@slack.com
Authentication-Results: gmx.net; notdkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Nothing),
("gmx.net".to_string(), DkimResult::Nothing)
]
);
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(actual, vec![("gmx.net".to_string(), DkimResult::Nothing)],);
// Weird Authentication-Results from Outlook without an authserv-id
let bytes = b"Authentication-Results: spf=pass (sender IP is 40.92.73.85)
smtp.mailfrom=hotmail.com; dkim=pass (signature was verified)
header.d=hotmail.com;dmarc=pass action=none
header.from=hotmail.com;compauth=pass reason=100";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "hotmail.com");
// At this point, the most important thing to test is that there are no
// authserv-ids with whitespace in them.
assert_eq!(
actual,
vec![("invalidAuthservId".to_string(), DkimResult::Passed)]
);
let bytes = b"Authentication-Results: gmx.net; dkim=none header.i=@slack.com
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Failed),
("gmx.net".to_string(), DkimResult::Passed)
]
);
// ';' in comments
let bytes = b"Authentication-Results: mx1.riseup.net;
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.a=rsa-sha256 header.s=mail header.b=avNJu6sw;
dkim-atps=neutral";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "yandex.ru");
assert_eq!(
actual,
vec![("mx1.riseup.net".to_string(), DkimResult::Passed)]
);
let bytes = br#"Authentication-Results: box.hispanilandia.net;
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
dkim-atps=neutral
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#;
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "disroot.org");
assert_eq!(
actual,
vec![
("box.hispanilandia.net".to_string(), DkimResult::Failed),
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_authservid_candidates() -> Result<()> {
let t = TestContext::new_alice().await;
update_authservid_candidates_test(&t, &["mx3.messagingengine.com"]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx3.messagingengine.com");
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
// A message without any Authentication-Results headers shouldn't remove all
// candidates since it could be a mailer-daemon message or so
update_authservid_candidates_test(&t, &[]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
update_authservid_candidates_test(&t, &["mx4.messagingengine.com", "someotherdomain.com"])
.await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
Ok(())
}
/// Calls update_authservid_candidates(), meant for using in a test.
///
/// update_authservid_candidates() only looks at the keys of its
/// `authentication_results` parameter. So, this function takes `incoming_ids`
/// and adds some AuthenticationResults to get the HashMap we need.
async fn update_authservid_candidates_test(context: &Context, incoming_ids: &[&str]) {
let v = incoming_ids
.iter()
.map(|id| (id.to_string(), DkimResult::Passed))
.collect();
update_authservid_candidates(context, &v).await.unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_realworld_authentication_results() -> Result<()> {
let mut test_failed = false;
let dir = tools::read_dir("test-data/message/dkimchecks-2022-09-28/".as_ref())
.await
.unwrap();
let mut bytes = Vec::new();
for entry in dir {
if !entry.file_type().await.unwrap().is_dir() {
continue;
}
let self_addr = entry.file_name().into_string().unwrap();
let self_domain = EmailAddress::new(&self_addr).unwrap().domain;
let authres_parsing_works = [
"ik.me",
"web.de",
"posteo.de",
"gmail.com",
"hotmail.com",
"mail.ru",
"aol.com",
"yahoo.com",
"icloud.com",
"fastmail.com",
"mail.de",
"outlook.com",
"gmx.de",
"testrun.org",
]
.contains(&self_domain.as_str());
let t = TestContext::new().await;
t.configure_addr(&self_addr).await;
if !authres_parsing_works {
println!("========= Receiving as {} =========", &self_addr);
}
// Simulate receiving all emails once, so that we have the correct authserv-ids
let mut dir = tools::read_dir(&entry.path()).await.unwrap();
// The ordering in which the emails are received can matter;
// the test _should_ pass for every ordering.
dir.sort_by_key(|d| d.file_name());
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::rng());
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let res = handle_authres(&t, &mail, from).await?;
let from_domain = EmailAddress::new(from).unwrap().domain;
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
// These are (fictional) forged emails where the attacker added a fake
// Authentication-Results before sending the email
&& from != "forged-authres-added@example.com"
// Other forged emails
&& !from.starts_with("forged");
if res.dkim_passed != expected_result {
if authres_parsing_works {
println!(
"!!!!!! FAILURE Receiving {:?} wrong result: !!!!!!",
entry.path(),
);
test_failed = true;
}
println!("From {}: {}", from_domain, res.dkim_passed);
}
}
}
assert!(!test_failed);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres() {
let t = TestContext::new().await;
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
// return an Err because this would prevent the message from being added
// to the database and downloaded again and again
let bytes = b"From: invalid@from.com
Authentication-Results: dkim=";
let mail = mailparse::parse_mail(bytes).unwrap();
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Bob knows his server's authserv-id
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
.await?;
let alice_bob_chat = alice.create_chat(&bob).await;
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
sent.payload
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
let rcvd = bob.recv_msg(&sent).await;
assert!(rcvd.error.is_none());
// Do the same without the mailing list header, this time the failed
// authres isn't ignored
let mut sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
let rcvd = bob.recv_msg(&sent).await;
// The message info should contain a warning:
assert!(
rcvd.id
.get_info(&bob)
.await
.unwrap()
.contains("DKIM Results: Passed=false")
);
Ok(())
}
}
+17 -25
View File
@@ -10,8 +10,8 @@ use anyhow::{Context as _, Result, ensure, format_err};
use base64::Engine as _;
use futures::StreamExt;
use image::ImageReader;
use image::codecs::jpeg::JpegEncoder;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
use num_traits::FromPrimitive;
use tokio::{fs, task};
use tokio_stream::wrappers::ReadDirStream;
@@ -284,10 +284,6 @@ impl<'a> BlobObject<'a> {
///
/// Recoding is only done for [`Viewtype::Image`]. For [`Viewtype::File`], if it's a correct
/// image, `*viewtype` is set to [`Viewtype::Image`].
///
/// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the
/// image is a true sticker assuming that it must have at least one fully transparent corner,
/// otherwise `*viewtype` is set to [`Viewtype::Image`].
pub async fn check_or_recode_image(
&mut self,
context: &Context,
@@ -362,7 +358,10 @@ impl<'a> BlobObject<'a> {
return Ok(name);
}
let mut img = imgreader.decode().context("image decode failure")?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let orientation = exif
.as_ref()
.map(|exif| exif_orientation(exif, context))
.unwrap_or(Orientation::NoTransforms);
let mut encoded = Vec::new();
if *vt == Viewtype::Sticker {
@@ -381,13 +380,7 @@ impl<'a> BlobObject<'a> {
return Ok(name);
}
}
img = match orientation {
Some(90) => img.rotate90(),
Some(180) => img.rotate180(),
Some(270) => img.rotate270(),
_ => img,
};
img.apply_orientation(orientation);
// max_wh is the maximum image width and height, i.e. the resolution-limit.
// target_wh target-resolution for resizing the image.
@@ -468,7 +461,7 @@ impl<'a> BlobObject<'a> {
));
}
target_wh = target_wh * 2 / 3;
target_wh = target_wh * 7 / 8;
} else {
info!(
context,
@@ -551,18 +544,17 @@ fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
Ok((len, exif))
}
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return 180,
Some(6) => return 90,
Some(8) => return 270,
other => warn!(context, "Exif orientation value ignored: {other:?}."),
}
fn exif_orientation(exif: &exif::Exif, context: &Context) -> Orientation {
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)
&& let Some(val) = orientation.value.get_uint(0)
&& let Ok(val) = TryInto::<u8>::try_into(val)
{
return Orientation::from_exif(val).unwrap_or({
warn!(context, "Exif orientation value ignored: {val:?}.");
Orientation::NoTransforms
});
}
0
Orientation::NoTransforms
}
/// All files in the blobdir.
+26 -6
View File
@@ -305,7 +305,7 @@ async fn test_recode_image_2() {
has_exif: true,
original_width: 2000,
original_height: 1800,
orientation: 270,
orientation: Some(Orientation::Rotate270),
compressed_width: 1800,
compressed_height: 2000,
..Default::default()
@@ -336,6 +336,28 @@ async fn test_recode_image_2() {
assert_correct_rotation(&img_rotated);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_vflipped() {
let bytes = include_bytes!("../../test-data/image/rectangle200x180-vflipped.jpg");
let img_rotated = SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
bytes,
extension: "jpg",
has_exif: true,
original_width: 200,
original_height: 180,
orientation: Some(Orientation::FlipVertical),
compressed_width: 200,
compressed_height: 180,
..Default::default()
}
.test()
.await
.unwrap();
assert_correct_rotation(&img_rotated);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_bad_exif() {
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
@@ -423,7 +445,6 @@ async fn test_recode_image_balanced_png() {
.await
.unwrap();
// This will be sent as Image, see [`BlobObject::check_or_recode_image()`] for explanation.
SendImageCheckMediaquality {
viewtype: Viewtype::Sticker,
media_quality_config: "0",
@@ -431,6 +452,7 @@ async fn test_recode_image_balanced_png() {
extension: "png",
original_width: 1920,
original_height: 1080,
res_viewtype: Some(Viewtype::Sticker),
compressed_width: 1920,
compressed_height: 1080,
..Default::default()
@@ -530,7 +552,7 @@ struct SendImageCheckMediaquality<'a> {
pub(crate) has_exif: bool,
pub(crate) original_width: u32,
pub(crate) original_height: u32,
pub(crate) orientation: i32,
pub(crate) orientation: Option<Orientation>,
pub(crate) res_viewtype: Option<Viewtype>,
pub(crate) compressed_width: u32,
pub(crate) compressed_height: u32,
@@ -546,7 +568,7 @@ impl SendImageCheckMediaquality<'_> {
let has_exif = self.has_exif;
let original_width = self.original_width;
let original_height = self.original_height;
let orientation = self.orientation;
let orientation = self.orientation.unwrap_or(Orientation::NoTransforms);
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
let compressed_width = self.compressed_width;
let compressed_height = self.compressed_height;
@@ -712,8 +734,6 @@ async fn test_send_gif_as_sticker() -> Result<()> {
let chat = alice.get_self_chat().await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
// extension.
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
Ok(())
}
+224 -216
View File
@@ -1,12 +1,11 @@
//! # Chat module.
use std::cmp;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::io::Cursor;
use std::marker::Sync;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow, bail, ensure};
@@ -23,8 +22,9 @@ use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE,
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
TIMESTAMP_SENT_TOLERANCE,
};
use crate::contact::{self, Contact, ContactId, Origin};
use crate::context::Context;
@@ -34,7 +34,7 @@ use crate::download::{
};
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
use crate::events::EventType;
use crate::key::self_fingerprint;
use crate::key::{Fingerprint, self_fingerprint};
use crate::location;
use crate::log::{LogExt, warn};
use crate::logged_debug_assert;
@@ -477,12 +477,17 @@ impl ChatId {
/// Adds message "Messages are end-to-end encrypted".
pub(crate) async fn add_e2ee_notice(self, context: &Context, timestamp: i64) -> Result<()> {
let text = stock_str::messages_e2ee_info_msg(context);
// Sort this notice to the very beginning of the chat.
// We don't want any message to appear before this notice
// which is normally added when encrypted chat is created.
let sort_timestamp = 0;
add_info_msg_with_cmd(
context,
self,
&text,
SystemMessage::ChatE2ee,
Some(timestamp),
Some(sort_timestamp),
timestamp,
None,
None,
@@ -492,6 +497,27 @@ impl ChatId {
Ok(())
}
/// Adds info message to the beginning of the chat.
///
/// Used for messages such as
/// "Others will only see this group after you sent a first message."
pub(crate) async fn add_start_info_message(self, context: &Context, text: &str) -> Result<()> {
let sort_timestamp = 0;
add_info_msg_with_cmd(
context,
self,
text,
SystemMessage::Unknown,
Some(sort_timestamp),
time(),
None,
None,
None,
)
.await?;
Ok(())
}
/// Archives or unarchives a chat.
pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
self.set_visibility_ex(context, Sync, visibility).await
@@ -925,6 +951,17 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
.unwrap_or(0))
}
/// Returns timestamp of us joining the chat if we are the member of the chat.
pub(crate) async fn join_timestamp(self, context: &Context) -> Result<Option<i64>> {
context
.sql
.query_get_value(
"SELECT add_timestamp FROM chats_contacts WHERE chat_id=? AND contact_id=?",
(self, ContactId::SELF),
)
.await
}
/// Returns timestamp of the latest message in the chat,
/// including hidden messages or a draft if there is one.
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
@@ -1151,7 +1188,6 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
/// prefer plaintext emails.
///
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
let chat = Chat::load_from_db(context, self).await?;
if !chat.is_encrypted(context).await? {
@@ -1174,7 +1210,8 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
);
let fingerprint = contact
.fingerprint()
.context("Contact does not have a fingerprint in encrypted chat")?;
.context("Contact does not have a fingerprint in encrypted chat")?
.human_readable();
if let Some(public_key) = contact.public_key(context).await? {
if let Some(relay_addrs) = addresses_from_public_key(&public_key) {
let relays = relay_addrs.join(",");
@@ -1212,15 +1249,11 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
/// corresponding event in case of a system message (usually the current system time).
/// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
/// to the chat bottom.
/// `received` -- whether the message is received. Otherwise being sent.
/// `incoming` -- whether the message is incoming.
pub(crate) async fn calc_sort_timestamp(
self,
context: &Context,
message_timestamp: i64,
always_sort_to_bottom: bool,
received: bool,
incoming: bool,
) -> Result<i64> {
let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
@@ -1240,38 +1273,6 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
(self, MessageState::OutDraft),
)
.await?
} else if received {
// Received messages shouldn't mingle with just sent ones and appear somewhere in the
// middle of the chat, so we go after the newest non fresh message.
//
// But if a received outgoing message is older than some seen message, better sort the
// received message purely by timestamp. We could place it just before that seen
// message, but anyway the user may not notice it.
//
// NB: Received outgoing messages may break sorting of fresh incoming ones, but this
// shouldn't happen frequently. Seen incoming messages don't really break sorting of
// fresh ones, they rather mean that older incoming messages are actually seen as well.
context
.sql
.query_row_optional(
"SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
FROM msgs
WHERE chat_id=? AND hidden=0 AND state>?
HAVING COUNT(*) > 0",
(MessageState::InSeen, self, MessageState::InFresh),
|row| {
let ts: i64 = row.get(0)?;
let ts_sent_seen: i64 = row.get(1)?;
Ok((ts, ts_sent_seen))
},
)
.await?
.and_then(|(ts, ts_sent_seen)| {
match incoming || ts_sent_seen <= message_timestamp {
true => Some(ts),
false => None,
}
})
} else {
None
};
@@ -1282,7 +1283,16 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
sort_timestamp = last_msg_time;
}
Ok(sort_timestamp)
if let Some(join_timestamp) = self.join_timestamp(context).await? {
// If we are the member of the chat, don't add messages
// before the timestamp of us joining it.
// This is needed to avoid sorting "Member added"
// or automatically sent bot welcome messages
// above SecureJoin system messages.
Ok(std::cmp::max(sort_timestamp, join_timestamp))
} else {
Ok(sort_timestamp)
}
}
}
@@ -1742,7 +1752,6 @@ impl Chat {
///
/// If `update_msg_id` is set, that record is reused;
/// if `update_msg_id` is None, a new record is created.
#[expect(clippy::arithmetic_side_effects)]
async fn prepare_msg_raw(
&mut self,
context: &Context,
@@ -2458,10 +2467,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
let mut maybe_image = false;
if msg.viewtype == Viewtype::File
|| msg.viewtype == Viewtype::Image
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
{
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
// Correct the type, take care not to correct already very special
// formats as GIF or VOICE.
//
@@ -2469,12 +2475,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
// - from FILE to AUDIO/VIDEO/IMAGE
// - from FILE/IMAGE to GIF */
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) {
if msg.viewtype == Viewtype::Sticker {
if better_type != Viewtype::Image {
// UIs don't want conversions of `Sticker` to anything other than `Image`.
msg.param.set_int(Param::ForceSticker, 1);
}
} else if better_type == Viewtype::Image {
if better_type == Viewtype::Image {
maybe_image = true;
} else if better_type != Viewtype::Webxdc
|| context
@@ -2494,10 +2495,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Vcard {
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
}
if msg.viewtype == Viewtype::File && maybe_image
|| msg.viewtype == Viewtype::Image
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
{
if msg.viewtype == Viewtype::File && maybe_image || msg.viewtype == Viewtype::Image {
let new_name = blob
.check_or_recode_image(context, msg.get_filename(), &mut msg.viewtype)
.await?;
@@ -2531,7 +2529,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
// running numbers, etc.
let filename: String = match viewtype_orig {
Viewtype::Voice => format!(
"voice-messsage_{}.{}",
"voice-messsage_{}.{suffix}",
chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0)
.single()
@@ -2539,10 +2537,9 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
),
&suffix
),
Viewtype::Image | Viewtype::Gif => format!(
"image_{}.{}",
"image_{}.{suffix}",
chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0)
.single()
@@ -2550,10 +2547,9 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
),
&suffix,
),
Viewtype::Video => format!(
"video_{}.{}",
"video_{}.{suffix}",
chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0)
.single()
@@ -2561,7 +2557,6 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
),
&suffix
),
_ => filename,
};
@@ -2615,10 +2610,10 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
"chat_id cannot be a special chat: {chat_id}"
);
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
if msg.state != MessageState::Undefined {
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
msg.update_param(context).await?;
// create_send_msg_jobs() will update `param` in the db.
}
// protect all system messages against RTLO attacks
@@ -2723,7 +2718,16 @@ async fn prepare_send_msg(
None
};
// ... then change the MessageState in the message object
if msg.state == MessageState::Undefined
// Legacy SecureJoin "v*-request" messages are unencrypted.
&& msg.param.get_cmd() != SystemMessage::SecurejoinMessage
&& chat.is_encrypted(context).await?
{
msg.param.set_int(Param::GuaranteeE2ee, 1);
if !msg.id.is_unset() {
msg.update_param(context).await?;
}
}
msg.state = MessageState::OutPending;
msg.timestamp_sort = create_smeared_timestamp(context);
@@ -2769,15 +2773,13 @@ async fn render_mime_message_and_pre_message(
let mut mimefactory_post_msg = mimefactory.clone();
mimefactory_post_msg.set_as_post_message();
let rendered_msg = mimefactory_post_msg
.render(context)
let rendered_msg = Box::pin(mimefactory_post_msg.render(context))
.await
.context("Failed to render post-message")?;
let mut mimefactory_pre_msg = mimefactory;
mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg);
let rendered_pre_msg = mimefactory_pre_msg
.render(context)
let rendered_pre_msg = Box::pin(mimefactory_pre_msg.render(context))
.await
.context("pre-message failed to render")?;
@@ -2792,7 +2794,7 @@ async fn render_mime_message_and_pre_message(
Ok((Some(rendered_pre_msg), rendered_msg))
} else {
Ok((None, mimefactory.render(context).await?))
Ok((None, Box::pin(mimefactory.render(context)).await?))
}
}
@@ -2921,11 +2923,26 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
msg.param.remove(Param::GuaranteeE2ee);
}
msg.subject.clone_from(&rendered_msg.subject);
// Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`.
context
.sql
.execute(
"UPDATE msgs SET pre_rfc724_mid=?, subject=?, param=? WHERE id=?",
"
UPDATE msgs SET
timestamp=(
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
-- From `InFresh` to `OutDelivered` inclusive, except `OutDraft`.
state IN(10,13,16,18,20,24,26) AND
hidden IN(0,1) AND
chat_id=? AND
id<=?
),
pre_rfc724_mid=?, subject=?, param=?
WHERE id=?
",
(
msg.chat_id,
msg.id,
&msg.pre_rfc724_mid,
&msg.subject,
msg.param.to_string(),
@@ -2990,7 +3007,6 @@ pub async fn send_text_msg(
}
/// Sends chat members a request to edit the given message's text.
#[expect(clippy::arithmetic_side_effects)]
pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> {
let mut original_msg = Message::load_from_db(context, msg_id).await?;
ensure!(
@@ -3075,9 +3091,6 @@ async fn donation_request_maybe(context: &Context) -> Result<()> {
/// Chat message list request options.
#[derive(Debug)]
pub struct MessageListOptions {
/// Return only info messages.
pub info_only: bool,
/// Add day markers before each date regarding the local timezone.
pub add_daymarker: bool,
}
@@ -3088,56 +3101,27 @@ pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<Cha
context,
chat_id,
MessageListOptions {
info_only: false,
add_daymarker: false,
},
)
.await
}
/// Returns messages belonging to the chat according to the given options.
/// Returns messages belonging to the chat according to the given options,
/// sorted by oldest message first.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_chat_msgs_ex(
context: &Context,
chat_id: ChatId,
options: MessageListOptions,
) -> Result<Vec<ChatItem>> {
let MessageListOptions {
info_only,
add_daymarker,
} = options;
let process_row = if info_only {
|row: &rusqlite::Row| {
// is_info logic taken from Message.is_info()
let params = row.get::<_, String>("param")?;
let (from_id, to_id) = (
row.get::<_, ContactId>("from_id")?,
row.get::<_, ContactId>("to_id")?,
);
let is_info_msg: bool = from_id == ContactId::INFO
|| to_id == ContactId::INFO
|| match Params::from_str(&params) {
Ok(p) => {
let cmd = p.get_cmd();
cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
}
_ => false,
};
Ok((
row.get::<_, i64>("timestamp")?,
row.get::<_, MsgId>("id")?,
!is_info_msg,
))
}
} else {
|row: &rusqlite::Row| {
Ok((
row.get::<_, i64>("timestamp")?,
row.get::<_, MsgId>("id")?,
false,
))
}
let MessageListOptions { add_daymarker } = options;
let process_row = |row: &rusqlite::Row| {
Ok((
row.get::<_, i64>("timestamp")?,
row.get::<_, MsgId>("id")?,
false,
))
};
let process_rows = |rows: rusqlite::AndThenRows<_>| {
// It is faster to sort here rather than
@@ -3172,39 +3156,18 @@ pub async fn get_chat_msgs_ex(
Ok(ret)
};
let items = if info_only {
context
.sql
.query_map(
// GLOB is used here instead of LIKE because it is case-sensitive
"SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id
FROM msgs m
WHERE m.chat_id=?
AND m.hidden=0
AND (
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
OR m.from_id == ?
OR m.to_id == ?
);",
(chat_id, ContactId::INFO, ContactId::INFO),
process_row,
process_rows,
)
.await?
} else {
context
.sql
.query_map(
"SELECT m.id AS id, m.timestamp AS timestamp
let items = context
.sql
.query_map(
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
WHERE m.chat_id=?
AND m.hidden=0;",
(chat_id,),
process_row,
process_rows,
)
.await?
};
(chat_id,),
process_row,
process_rows,
)
.await?;
Ok(items)
}
@@ -3622,7 +3585,7 @@ pub(crate) async fn create_group_ex(
// Add "Messages in this chat use classic email and are not encrypted." message.
stock_str::chat_unencrypted_explanation(context)
};
add_info_msg(context, chat_id, &text).await?;
chat_id.add_start_info_message(context, &text).await?;
}
if let (true, true) = (sync.into(), !grpid.is_empty()) {
let id = SyncId::Grpid(grpid);
@@ -3753,7 +3716,7 @@ pub(crate) async fn update_chat_contacts_table(
context: &Context,
timestamp: i64,
id: ChatId,
contacts: &HashSet<ContactId>,
contacts: &BTreeSet<ContactId>,
) -> Result<()> {
context
.sql
@@ -3979,9 +3942,47 @@ pub(crate) async fn add_contact_to_chat_ex(
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
}
if chat.typ == Chattype::OutBroadcast {
resend_last_msgs(context, chat.id, &contact)
.await
.log_err(context)
.ok();
}
Ok(true)
}
async fn resend_last_msgs(context: &Context, chat_id: ChatId, to_contact: &Contact) -> Result<()> {
let msgs: Vec<MsgId> = context
.sql
.query_map_vec(
"
SELECT id
FROM msgs
WHERE chat_id=?
AND hidden=0
AND NOT ( -- Exclude info and system messages
param GLOB '*\nS=*' OR param GLOB 'S=*'
OR from_id=?
OR to_id=?
)
AND type!=?
ORDER BY timestamp DESC, id DESC LIMIT ?",
(
chat_id,
ContactId::INFO,
ContactId::INFO,
Viewtype::Webxdc,
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER,
),
|row: &rusqlite::Row| Ok(row.get::<_, MsgId>(0)?),
)
.await?
.into_iter()
.rev()
.collect();
resend_msgs_ex(context, &msgs, to_contact.fingerprint()).await
}
/// Returns true if an avatar should be attached in the given chat.
///
/// This function does not check if the avatar is set.
@@ -4121,61 +4122,56 @@ pub async fn remove_contact_from_chat(
delete_broadcast_secret(context, chat_id).await?;
}
if matches!(
chat.typ,
Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast
) {
if !chat.is_self_in_chat(context).await? {
let err_msg = format!(
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
);
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
bail!("{err_msg}");
} else {
let mut sync = Nosync;
ensure!(
matches!(
chat.typ,
Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast
),
"Cannot remove members from non-group chats."
);
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
} else {
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?;
}
if !chat.is_self_in_chat(context).await? {
let err_msg =
format!("Cannot remove contact {contact_id} from chat {chat_id}: self not in group.");
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
bail!("{err_msg}");
}
// We do not return an error if the contact does not exist in the database.
// This allows to delete dangling references to deleted contacts
// in case of the database becoming inconsistent due to a bug.
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
if chat.is_promoted() {
let addr = contact.get_addr();
let fingerprint = contact.fingerprint().map(|f| f.hex());
let mut sync = Nosync;
let res = send_member_removal_msg(
context,
&chat,
contact_id,
addr,
fingerprint.as_deref(),
)
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
} else {
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?;
}
// We do not return an error if the contact does not exist in the database.
// This allows to delete dangling references to deleted contacts
// in case of the database becoming inconsistent due to a bug.
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
if chat.is_promoted() {
let addr = contact.get_addr();
let fingerprint = contact.fingerprint().map(|f| f.hex());
let res =
send_member_removal_msg(context, &chat, contact_id, addr, fingerprint.as_deref())
.await;
if contact_id == ContactId::SELF {
res?;
} else if let Err(e) = res {
warn!(
context,
"remove_contact_from_chat({chat_id}, {contact_id}): send_msg() failed: {e:#}."
);
}
} else {
sync = Sync;
}
}
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
if contact_id == ContactId::SELF {
res?;
} else if let Err(e) = res {
warn!(
context,
"remove_contact_from_chat({chat_id}, {contact_id}): send_msg() failed: {e:#}."
);
}
} else {
sync = Sync;
}
} else {
bail!("Cannot remove members from non-group chats.");
}
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
}
Ok(())
@@ -4537,6 +4533,7 @@ pub async fn forward_msgs_2ctx(
msg.state = MessageState::OutPending;
msg.rfc724_mid = create_outgoing_rfc724_mid();
msg.pre_rfc724_mid.clear();
msg.timestamp_sort = curr_timestamp;
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
@@ -4584,7 +4581,6 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
/// the copy contains a reference to the original message
/// as well as to the original chat in case the original message gets deleted.
/// Returns data needed to add a `SaveMessage` sync item.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn save_copy_in_self_talk(
context: &Context,
src_msg_id: MsgId,
@@ -4651,10 +4647,26 @@ pub(crate) async fn save_copy_in_self_talk(
Ok(msg.rfc724_mid)
}
/// Resends given messages with the same Message-ID.
/// Resends given messages to members of the corresponding chats.
///
/// This is primarily intended to make existing webxdcs available to new chat members.
pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
resend_msgs_ex(context, msg_ids, None).await
}
/// Resends given messages to a contact with fingerprint `to_fingerprint` or, if it's `None`, to
/// members of the corresponding chats.
///
/// NB: Actually `to_fingerprint` is only passed for `OutBroadcast` chats when a new member is
/// added. Regarding webxdcs: It is not trivial to resend only the own status updates,
/// and it is not trivial to resend them only to the newly-joined member,
/// so that for now, [`resend_last_msgs`] does not automatically resend webxdcs at all.
pub(crate) async fn resend_msgs_ex(
context: &Context,
msg_ids: &[MsgId],
to_fingerprint: Option<Fingerprint>,
) -> Result<()> {
let to_fingerprint = to_fingerprint.map(|f| f.hex());
let mut msgs: Vec<Message> = Vec::new();
for msg_id in msg_ids {
let msg = Message::load_from_db(context, *msg_id).await?;
@@ -4673,10 +4685,17 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
| MessageState::OutFailed
| MessageState::OutDelivered
| MessageState::OutMdnRcvd => {
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
// Broadcast owners shouldn't see spinners on messages being auto-re-sent to new
// subscribers (otherwise big channel owners will see spinners most of the time).
if to_fingerprint.is_none() {
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
}
}
msg_state => bail!("Unexpected message state {msg_state}"),
}
if let Some(to_fingerprint) = &to_fingerprint {
msg.param.set(Param::Arg4, to_fingerprint.clone());
}
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
continue;
}
@@ -4688,7 +4707,8 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
chat_id: msg.chat_id,
msg_id: msg.id,
});
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
// The event only matters if the message is last in the chat.
// But it's probably too expensive check, and UIs anyways need to debounce.
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
if msg.viewtype == Viewtype::Webxdc {
@@ -4881,8 +4901,6 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
// no wrong information are shown in the device chat
// - deletion in `devmsglabels` makes sure,
// deleted messages are reset and useful messages can be added again
// - we reset the config-option `QuotaExceeding`
// that is used as a helper to drive the corresponding device message.
pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
context
.sql
@@ -4898,9 +4916,6 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
(),
)
.await?;
context
.set_config_internal(Config::QuotaExceeding, None)
.await?;
Ok(())
}
@@ -4938,15 +4953,8 @@ pub(crate) async fn add_info_msg_with_cmd(
ts
} else {
let sort_to_bottom = true;
let (received, incoming) = (false, false);
chat_id
.calc_sort_timestamp(
context,
smeared_time(context),
sort_to_bottom,
received,
incoming,
)
.calc_sort_timestamp(context, smeared_time(context), sort_to_bottom)
.await?
};
@@ -5024,7 +5032,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
chat.typ == Chattype::OutBroadcast,
"{id} is not a broadcast list",
);
let mut contacts = HashSet::new();
let mut contacts = BTreeSet::new();
for addr in addrs {
let contact_addr = ContactAddress::new(addr)?;
let contact = Contact::add_or_lookup(context, "", &contact_addr, Origin::Hidden)
@@ -5032,7 +5040,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
.0;
contacts.insert(contact);
}
let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
let contacts_old = BTreeSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
if contacts == contacts_old {
return Ok(());
}
+299 -65
View File
@@ -3,11 +3,11 @@ use std::sync::Arc;
use super::*;
use crate::Event;
use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS, N_MSGS_TO_NEW_BROADCAST_MEMBER};
use crate::ephemeral::Timer;
use crate::headerdef::HeaderDef;
use crate::imex::{ImexMode, has_backup, imex};
use crate::message::{MessengerMessage, delete_msgs};
use crate::message::{Message, MessengerMessage, delete_msgs};
use crate::mimeparser::{self, MimeMessage};
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
@@ -2032,12 +2032,6 @@ async fn test_classic_email_chat() -> Result<()> {
let msgs = get_chat_msgs(&alice, chat_id).await?;
assert_eq!(msgs.len(), 1);
// Alice disables receiving classic emails.
alice
.set_config(Config::ShowEmails, Some("0"))
.await
.unwrap();
// Already received classic email should still be in the chat.
assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1);
@@ -2075,13 +2069,7 @@ async fn test_chat_get_color_encrypted() -> Result<()> {
Ok(())
}
async fn test_sticker(
filename: &str,
bytes: &[u8],
res_viewtype: Viewtype,
w: i32,
h: i32,
) -> Result<()> {
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
@@ -2097,7 +2085,7 @@ async fn test_sticker(
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, bob_chat.id);
assert_eq!(msg.get_viewtype(), res_viewtype);
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
assert_eq!(msg.get_filename().unwrap(), filename);
assert_eq!(msg.get_width(), w);
assert_eq!(msg.get_height(), h);
@@ -2111,7 +2099,6 @@ async fn test_sticker_png() -> Result<()> {
test_sticker(
"sticker.png",
include_bytes!("../../test-data/image/logo.png"),
Viewtype::Sticker,
135,
135,
)
@@ -2123,7 +2110,6 @@ async fn test_sticker_jpeg() -> Result<()> {
test_sticker(
"sticker.jpg",
include_bytes!("../../test-data/image/avatar1000x1000.jpg"),
Viewtype::Image,
1000,
1000,
)
@@ -2131,10 +2117,33 @@ async fn test_sticker_jpeg() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_jpeg_force() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
async fn test_sticker_gif() -> Result<()> {
test_sticker(
"sticker.gif",
include_bytes!("../../test-data/image/logo.gif"),
135,
135,
)
.await
}
/// Tests that stickers are sent as stickers.
///
/// Previously there was heuristic that stickers
/// were sometimes turned into non-stickers,
/// e.g. when it looked like UI sent
/// a screenshot dragged from the gallery into chat
/// as a sticker.
///
/// We have no such heuristic anymore,
/// if such heuristic is needed on some platform,
/// UI code should implement it.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_no_heuristics() {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat = alice.create_chat(bob).await;
let file = alice.get_blobdir().join("sticker.jpg");
tokio::fs::write(
@@ -2144,53 +2153,38 @@ async fn test_sticker_jpeg_force() {
.await
.unwrap();
// Images without force_sticker should be turned into [Viewtype::Image]
// Send a sticker.
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
.unwrap();
let file = msg.get_file(&alice).unwrap();
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_viewtype(), Viewtype::Image);
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
.unwrap();
msg.force_sticker();
let file = msg.get_file(alice).unwrap();
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
// even on drafted messages
// Send a sticker reusing the file.
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
.unwrap();
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
// Set sticker as a draft, then send it.
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
.unwrap();
msg.force_sticker();
alice_chat
.id
.set_draft(&alice, Some(&mut msg))
.set_draft(alice, Some(&mut msg))
.await
.unwrap();
let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap();
let mut msg = alice_chat.id.get_draft(alice).await.unwrap().unwrap();
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_gif() -> Result<()> {
test_sticker(
"sticker.gif",
include_bytes!("../../test-data/image/logo.gif"),
Viewtype::Sticker,
135,
135,
)
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_forward() -> Result<()> {
// create chats
@@ -2692,6 +2686,49 @@ async fn test_resend_own_message() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_doesnt_resort_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_grp = create_group(alice, "").await?;
let sent1 = alice.send_text(alice_grp, "hi").await;
let sent1_ts = Message::load_from_db(alice, sent1.sender_msg_id)
.await?
.timestamp_sort;
SystemTime::shift(Duration::from_secs(60));
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
let sent2 = alice
.send_text(
alice_grp,
"Let's test resending, there are very few tests on it",
)
.await;
let resent_msg_id = sent1.sender_msg_id;
resend_msgs(alice, &[resent_msg_id]).await?;
assert_eq!(
resent_msg_id.get_state(alice).await?,
MessageState::OutPending
);
alice.pop_sent_msg().await;
assert_eq!(
resent_msg_id.get_state(alice).await?,
MessageState::OutDelivered
);
assert_eq!(
Message::load_from_db(alice, sent1.sender_msg_id)
.await?
.timestamp_sort,
sent1_ts
);
assert_eq!(
alice.get_last_msg_id_in(alice_grp).await,
sent2.sender_msg_id
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_foreign_message_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -2792,7 +2829,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
let parsed_by_bob = bob.parse_msg(&vc_pubkey).await;
assert!(parsed_by_bob.decrypting_failed);
assert!(parsed_by_bob.decryption_error.is_some());
charlie.recv_msg_trash(&vc_pubkey).await;
}
@@ -2805,6 +2842,15 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
"alice@example.org charlie@example.net"
);
// Check additionally that subscribers don't send "Chat-Group-Name*" headers.
let parsed = alice.parse_msg(&request_with_auth).await;
assert!(parsed.get_header(HeaderDef::ChatGroupName).is_none());
assert!(
parsed
.get_header(HeaderDef::ChatGroupNameTimestamp)
.is_none()
);
alice.recv_msg_trash(&request_with_auth).await;
}
@@ -2821,7 +2867,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
let parsed_by_bob = bob.parse_msg(&member_added).await;
assert!(parsed_by_bob.decrypting_failed);
assert!(parsed_by_bob.decryption_error.is_some());
let rcvd = charlie.recv_msg(&member_added).await;
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberAddedToGroup);
@@ -2836,7 +2882,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
let parsed_by_bob = bob.parse_msg(&hi_msg).await;
assert_eq!(parsed_by_bob.decrypting_failed, false);
assert!(parsed_by_bob.decryption_error.is_none());
}
tcm.section("Alice removes Charlie. Bob must not see it.");
@@ -2853,7 +2899,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
let parsed_by_bob = bob.parse_msg(&member_removed).await;
assert!(parsed_by_bob.decrypting_failed);
assert!(parsed_by_bob.decryption_error.is_some());
let rcvd = charlie.recv_msg(&member_removed).await;
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberRemovedFromGroup);
@@ -2947,6 +2993,56 @@ async fn test_broadcast_change_name() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_broadcast_resend_to_new_member() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_bc_id = create_broadcast(alice, "Channel".to_string()).await?;
let qr = get_securejoin_qr(alice, Some(alice_bc_id)).await.unwrap();
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let mut alice_msg_ids = Vec::new();
for i in 0..(N_MSGS_TO_NEW_BROADCAST_MEMBER + 1) {
alice_msg_ids.push(
alice
.send_text(alice_bc_id, &i.to_string())
.await
.sender_msg_id,
);
}
let fiona_bc_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await;
for msg_id in alice_msg_ids {
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutDelivered);
}
for i in 0..N_MSGS_TO_NEW_BROADCAST_MEMBER {
let rev_order = false;
let resent_msg = alice
.pop_sent_msg_ex(rev_order, Duration::ZERO)
.await
.unwrap();
let fiona_msg = fiona.recv_msg(&resent_msg).await;
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
assert_eq!(fiona_msg.text, (i + 1).to_string());
assert!(resent_msg.recipients.contains("fiona@example.net"));
assert!(!resent_msg.recipients.contains("bob@"));
// The message is undecryptable for Bob, he mustn't be able to know yet that somebody joined
// the broadcast even if he is a postman in this land. E.g. Fiona may leave after fetching
// the news, Bob won't know about that.
assert!(
MimeMessage::from_bytes(bob, resent_msg.payload().as_bytes())
.await?
.decryption_error
.is_some()
);
bob.recv_msg_trash(&resent_msg).await;
}
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
Ok(())
}
/// - Alice has multiple devices
/// - Alice creates a broadcast and sends a message into it
/// - Alice's second device sees the broadcast
@@ -3768,14 +3864,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
// The contact should be marked as verified.
check_direct_chat_is_hidden_and_contact_is_verified(alice, bob0).await;
check_direct_chat_is_hidden_and_contact_is_verified(bob0, alice).await;
// TODO: There is a known bug in `observe_securejoin_on_other_device()`:
// When Bob joins a group or broadcast with his first device,
// then a chat with Alice will pop up on his second device.
// When it's fixed, the 2 following lines can be replaced with
// `check_direct_chat_is_hidden_and_contact_is_verified(bob1, alice).await;`
let bob1_alice_contact = bob1.add_or_lookup_contact_no_key(alice).await;
assert!(bob1_alice_contact.is_verified(bob1).await.unwrap());
check_direct_chat_is_hidden_and_contact_is_verified(bob1, alice).await;
tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
@@ -5034,6 +5123,31 @@ async fn test_do_not_overwrite_draft() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_msg_after_another_from_future() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let chat_id = t.get_self_chat().await.id;
// Simulate sending a message with clock set to the future.
SystemTime::shift(Duration::from_secs(3600));
let msg_id = send_text_msg(t, chat_id, "test".to_string()).await?;
SystemTime::shift_back(Duration::from_secs(3600));
let timestamp_sent: i64 = t
.sql
.query_get_value("SELECT timestamp_sent FROM msgs WHERE id=?", (msg_id,))
.await?
.unwrap();
// Let's have a check here that locally sent messages have zero `timestamp_sent`, it can be a
// useful invariant.
assert_eq!(timestamp_sent, 0);
let msg_id = send_text_msg(t, chat_id, "Fixed my clock".to_string()).await?;
assert_eq!(t.get_last_msg_in(chat_id).await.id, msg_id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_info_contact_id() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -5702,7 +5816,7 @@ async fn test_send_delete_request() -> Result<()> {
let sent2 = alice.pop_sent_msg().await;
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
// Bob receives both messages and has nothing the end
// Bob receives both messages and has nothing at the end
let bob_msg = bob.recv_msg(&sent1).await;
assert_eq!(bob_msg.text, "wtf");
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 2);
@@ -5710,6 +5824,11 @@ async fn test_send_delete_request() -> Result<()> {
bob.recv_msg_opt(&sent2).await;
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
// ... even if he receives messages in reverse order.
let bob2 = &tcm.bob().await;
bob2.recv_msg_opt(&sent2).await;
assert!(bob2.recv_msg_opt(&sent1).await.is_none());
// Alice has another device, and there is also nothing at the end
let alice2 = &tcm.alice().await;
alice2.recv_msg(&sent0).await;
@@ -6112,3 +6231,118 @@ async fn test_leftgrps() -> Result<()> {
Ok(())
}
/// Tests that if the message arrives late,
/// it can still be sorted above the last seen message.
///
/// Versions 2.47 and below always sorted incoming messages
/// after the last seen message, but with
/// the introduction of multi-relay it is possible
/// that some messages arrive only to some relays
/// and are fetched after the already arrived seen message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_late_message_above_seen() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_chat_id = alice
.create_group_with_members("Group", &[bob, charlie])
.await;
let alice_sent = alice.send_text(alice_chat_id, "Hello everyone!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
bob_chat_id.accept(bob).await?;
let charlie_chat_id = charlie.recv_msg(&alice_sent).await.chat_id;
charlie_chat_id.accept(charlie).await?;
// Bob sends a message, but the message is delayed.
let bob_sent = bob.send_text(bob_chat_id, "Hello from Bob!").await;
SystemTime::shift(Duration::from_secs(1000));
let charlie_sent = charlie
.send_text(charlie_chat_id, "Hello from Charlie!")
.await;
// Alice immediately receives a message from Charlie and reads it.
let alice_received_from_charlie = alice.recv_msg(&charlie_sent).await;
assert_eq!(
alice_received_from_charlie.get_text(),
"Hello from Charlie!"
);
message::markseen_msgs(alice, vec![alice_received_from_charlie.id]).await?;
// Bob message arrives later, it should be above the message from Charlie.
let alice_received_from_bob = alice.recv_msg(&bob_sent).await;
assert_eq!(alice_received_from_bob.get_text(), "Hello from Bob!");
// The last message in the chat is still from Charlie.
let last_msg = alice.get_last_msg_in(alice_chat_id).await;
assert_eq!(last_msg.get_text(), "Hello from Charlie!");
Ok(())
}
/// Tests that start message for unpromoted groups sticks to the top of the chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unpromoted_group_start_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// Start messages are disabled for test context by default,
// but this test is specifically about start messages.
alice.set_config(Config::SkipStartMessages, None).await?;
// Shift the clock forward, so we can rewind it back later.
SystemTime::shift(Duration::from_secs(3600));
// Alice creates unpromoted group with Bob.
let chat_id = create_group(alice, "Group").await?;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, chat_id, bob_id).await?;
let [
ChatItem::Message {
msg_id: e2ee_msg_id,
},
ChatItem::Message {
msg_id: info_msg_id,
},
] = get_chat_msgs(alice, chat_id).await?[..]
else {
panic!("Expected two messages in the chat");
};
let msg = Message::load_from_db(alice, e2ee_msg_id).await?;
assert_eq!(msg.text, "Messages are end-to-end encrypted.");
let msg = Message::load_from_db(alice, info_msg_id).await?;
assert_eq!(
msg.text,
"Others will only see this group after you sent a first message."
);
// Alice rewinds the clock.
SystemTime::shift_back(Duration::from_secs(3600));
let text_msg_id = send_text_msg(alice, chat_id, "Hello".to_string()).await?;
let [
ChatItem::Message {
msg_id: e2ee_msg_id2,
},
ChatItem::Message {
msg_id: info_msg_id2,
},
ChatItem::Message {
msg_id: text_msg_id2,
},
] = get_chat_msgs(alice, chat_id).await?[..]
else {
panic!("Expected three messages in the chat");
};
assert_eq!(e2ee_msg_id2, e2ee_msg_id);
assert_eq!(info_msg_id2, info_msg_id);
assert_eq!(text_msg_id2, text_msg_id);
Ok(())
}
+5 -5
View File
@@ -142,7 +142,7 @@ impl Chatlist {
AND c.blocked!=1
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
ORDER BY c.archived=?3 DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
process_row,
).await?
@@ -168,7 +168,7 @@ impl Chatlist {
AND c.blocked!=1
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft,),
process_row,
)
@@ -204,7 +204,7 @@ impl Chatlist {
AND IFNULL(c.name_normalized,c.name) LIKE ?3
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
process_row,
)
@@ -253,7 +253,7 @@ impl Chatlist {
AND NOT c.archived=?
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
GROUP BY c.id
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
(
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
Chattype::Group, ContactId::SELF,
@@ -279,7 +279,7 @@ impl Chatlist {
AND (c.blocked=0 OR c.blocked=2)
AND NOT c.archived=?
GROUP BY c.id
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
process_row,
).await?
+99 -129
View File
@@ -42,50 +42,85 @@ use crate::{constants, stats};
)]
#[strum(serialize_all = "snake_case")]
pub enum Config {
/// Deprecated(2026-04).
/// Use ConfiguredAddr, [`crate::login_param::EnteredLoginParam`],
/// or add_transport{from_qr}()/list_transports() instead.
///
/// Email address, used in the `From:` field.
Addr,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server hostname.
MailServer,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server username.
MailUser,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server password.
MailPw,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server port.
MailPort,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// IMAP server security (e.g. TLS, STARTTLS).
MailSecurity,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// How to check TLS certificates.
///
/// "IMAP" in the name is for compatibility,
/// this actually applies to both IMAP and SMTP connections.
ImapCertificateChecks,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server hostname.
SendServer,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server username.
SendUser,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server password.
SendPw,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server port.
SendPort,
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// SMTP server security (e.g. TLS, STARTTLS).
SendSecurity,
/// Deprecated option for backwards compatibility.
/// Deprecated(2026-04).
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
SmtpCertificateChecks,
/// Whether to use OAuth 2.
///
/// Historically contained other bitflags, which are now deprecated.
@@ -155,37 +190,10 @@ pub enum Config {
#[strum(props(default = "1"))]
MdnsEnabled,
/// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
/// ones are moved there anyway.
#[strum(props(default = "1"))]
MvboxMove,
/// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only.
///
/// This will not entirely disable other folders, e.g. the spam folder will also still
/// be watched for new messages.
#[strum(props(default = "0"))]
OnlyFetchMvbox,
/// Whether to show classic emails or only chat messages.
#[strum(props(default = "2"))] // also change ShowEmails.default() on changes
ShowEmails,
/// Quality of the media files to send.
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
/// Timer in seconds after which the message is deleted from the
/// server.
///
/// 0 means messages are never deleted by Delta Chat.
///
/// Value 1 is treated as "delete at once": messages are deleted
/// immediately, without moving to DeltaChat folder.
///
/// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise.
DeleteServerAfter,
/// Timer in seconds after which the message is deleted from the
/// device.
///
@@ -197,32 +205,47 @@ pub enum Config {
/// The primary email address.
ConfiguredAddr,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// List of configured IMAP servers as a JSON array.
ConfiguredImapServers,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured IMAP server hostname.
///
/// This is replaced by `configured_imap_servers` for new configurations.
ConfiguredMailServer,
/// Configured IMAP server port.
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// This is replaced by `configured_imap_servers` for new configurations.
/// Configured IMAP server port.
ConfiguredMailPort,
/// Configured IMAP server security (e.g. TLS, STARTTLS).
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// This is replaced by `configured_imap_servers` for new configurations.
/// Configured IMAP server security (e.g. TLS, STARTTLS).
ConfiguredMailSecurity,
/// Configured IMAP server username.
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// This is set if user has configured username manually.
/// Configured IMAP server username.
ConfiguredMailUser,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured IMAP server password.
ConfiguredMailPw,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured TLS certificate checks.
/// This option is saved on successful configuration
/// and should not be modified manually.
@@ -231,52 +254,68 @@ pub enum Config {
/// but has "IMAP" in the name for backwards compatibility.
ConfiguredImapCertificateChecks,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// List of configured SMTP servers as a JSON array.
ConfiguredSmtpServers,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server hostname.
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendServer,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server port.
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendPort,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server security (e.g. TLS, STARTTLS).
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendSecurity,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server username.
///
/// This is set if user has configured username manually.
ConfiguredSendUser,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// Configured SMTP server password.
ConfiguredSendPw,
/// Deprecated, stored for backwards compatibility.
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
/// ConfiguredImapCertificateChecks is actually used.
ConfiguredSmtpCertificateChecks,
/// Whether OAuth 2 is used with configured provider.
ConfiguredServerFlags,
/// Configured folder for incoming messages.
ConfiguredInboxFolder,
/// Configured folder for chat messages.
ConfiguredMvboxFolder,
/// Unix timestamp of the last successful configuration.
ConfiguredTimestamp,
/// ID of the configured provider from the provider database.
ConfiguredProvider,
/// Deprecated(2026-04).
/// Use [`Context::is_configured()`] instead.
///
/// True if account is configured.
Configured,
@@ -317,11 +356,6 @@ pub enum Config {
#[strum(props(default = "0"))]
NotifyAboutWrongPw,
/// If a warning about exceeding quota was shown recently,
/// this is the percentage of quota at the time the warning was given.
/// Unset, when quota falls below minimal warning threshold again.
QuotaExceeding,
/// Timestamp of the last time housekeeping was run
LastHousekeeping,
@@ -362,15 +396,6 @@ pub enum Config {
#[strum(props(default = "1"))]
SyncMsgs,
/// Space-separated list of all the authserv-ids which we believe
/// may be the one of our email server.
///
/// See `crate::authres::update_authservid_candidates`.
AuthservIdCandidates,
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]
@@ -465,21 +490,13 @@ impl Config {
pub(crate) fn is_synced(&self) -> bool {
matches!(
self,
Self::Displayname
| Self::MdnsEnabled
| Self::MvboxMove
| Self::ShowEmails
| Self::Selfavatar
| Self::Selfstatus,
Self::Displayname | Self::MdnsEnabled | Self::Selfavatar | Self::Selfstatus,
)
}
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
)
matches!(self, Config::ConfiguredAddr)
}
}
@@ -526,14 +543,6 @@ impl Context {
// Default values
let val = match key {
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
Config::DeleteServerAfter => {
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
&& Box::pin(self.is_chatmail()).await?
{
true => Some("1".to_string()),
false => Some("0".to_string()),
}
}
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
_ => key.get_str("default").map(|s| s.to_string()),
};
@@ -594,13 +603,6 @@ impl Context {
.is_some_and(|x| x != 0))
}
/// Returns true if movebox ("DeltaChat" folder) should be watched.
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?
|| !self.get_config_bool(Config::IsChatmail).await?)
}
/// Returns true if sync messages should be sent.
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SyncMsgs).await?
@@ -621,23 +623,6 @@ impl Context {
self.get_config_bool(Config::MdnsEnabled).await
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
/// at once, `Some(x)` means delete after `x` seconds.
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
let val = match self
.get_config_parsed::<i64>(Config::DeleteServerAfter)
.await?
.unwrap_or(0)
{
0 => None,
1 => Some(0),
x => Some(x),
};
Ok(val)
}
/// Gets the configured provider.
///
/// The provider is determined by the current primary transport.
@@ -682,13 +667,10 @@ impl Context {
| Config::ProxyEnabled
| Config::BccSelf
| Config::MdnsEnabled
| Config::MvboxMove
| Config::OnlyFetchMvbox
| Config::Configured
| Config::Bot
| Config::NotifyAboutWrongPw
| Config::SyncMsgs
| Config::SignUnencrypted
| Config::DisableIdle => {
ensure!(
matches!(value, None | Some("0") | Some("1")),
@@ -706,11 +688,6 @@ impl Context {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
Self::check_config(key, value)?;
let n_transports = self.count_transports().await?;
if n_transports > 1 && matches!(key, Config::MvboxMove | Config::OnlyFetchMvbox) {
bail!("Cannot reconfigure {key} when multiple transports are configured");
}
let _pause = match key.needs_io_restart() {
true => self.scheduler.pause(self).await?,
_ => Default::default(),
@@ -789,12 +766,6 @@ impl Context {
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
.await?;
}
Config::MvboxMove => {
self.sql.set_raw_config(key.as_ref(), value).await?;
self.sql
.set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
.await?;
}
Config::ConfiguredAddr => {
let Some(addr) = value else {
bail!("Cannot unset configured_addr");
@@ -933,16 +904,23 @@ impl Context {
/// Determine whether the specified addr maps to the/a self addr.
/// Returns `false` if no addresses are configured.
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
// Employ the config cache to optimize for `ConfiguredAddr` passed.
if !addr.is_empty()
&& addr_cmp(
addr,
&self
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
)
{
return Ok(true);
}
Ok(self
.get_config(Config::ConfiguredAddr)
.get_all_self_addrs()
.await?
.iter()
.any(|a| addr_cmp(addr, a))
|| self
.get_secondary_self_addrs()
.await?
.iter()
.any(|a| addr_cmp(addr, a)))
.any(|a| addr_cmp(addr, a)))
}
/// Sets `primary_new` as the new primary self address and saves the old
@@ -989,14 +967,6 @@ impl Context {
.await
}
/// Returns all secondary self addresses.
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
let addr: String = row.get(0)?;
Ok(addr)
}).await
}
/// Returns all published secondary self addresses.
/// See `[Context::set_transport_unpublished]`
pub(crate) async fn get_published_secondary_self_addrs(&self) -> Result<Vec<String>> {
-29
View File
@@ -142,28 +142,6 @@ async fn test_mdns_default_behaviour() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_server_after_default() -> Result<()> {
let t = &TestContext::new_alice().await;
// Check that the settings are displayed correctly.
assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
assert_eq!(
t.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
// Leaving emails on the server even w/o `BccSelf` is a good default at least because other
// MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
// does).
t.set_config_bool(Config::BccSelf, false).await?;
assert_eq!(
t.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
Ok(())
}
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -196,13 +174,6 @@ async fn test_sync() -> Result<()> {
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
for key in [Config::ShowEmails, Config::MvboxMove] {
let val = alice0.get_config_bool(key).await?;
alice0.set_config_bool(key, !val).await?;
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(key).await?, !val);
}
// `Config::SyncMsgs` mustn't be synced.
alice0.set_config_bool(Config::SyncMsgs, false).await?;
alice0.set_config_bool(Config::SyncMsgs, true).await?;
+16 -42
View File
@@ -76,7 +76,7 @@ impl Context {
/// Deprecated since 2025-02; use `add_transport_from_qr()`
/// or `add_or_update_transport()` instead.
pub async fn configure(&self) -> Result<()> {
let mut param = EnteredLoginParam::load(self).await?;
let mut param = EnteredLoginParam::load_legacy(self).await?;
self.add_transport_inner(&mut param).await
}
@@ -150,7 +150,7 @@ impl Context {
progress!(self, 0, Some(error_msg.clone()));
bail!(error_msg);
} else {
param.save(self).await?;
param.save_legacy(self).await?;
progress!(self, 1000);
}
@@ -243,11 +243,6 @@ impl Context {
Ok((id, add_timestamp))
},
)?;
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
transaction.execute(
"DELETE FROM imap_sync WHERE transport_id=?",
(transport_id,),
)?;
// Removal timestamp should not be lower than addition timestamp
// to be accepted by other devices when synced.
@@ -266,6 +261,7 @@ impl Context {
.await?;
send_sync_transports(self).await?;
self.quota.write().await.remove(&removed_transport_id);
self.restart_io_if_running().await;
Ok(())
}
@@ -320,31 +316,16 @@ impl Context {
(&param.addr,),
)
.await?
{
// Should be checked before `MvboxMove` because the latter makes no sense in presense of
// `OnlyFetchMvbox` and even grayed out in the UIs in this case.
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
bail!(
"To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
);
}
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
bail!(
"To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
);
}
if self
&& self
.sql
.count("SELECT COUNT(*) FROM transports", ())
.await?
>= MAX_TRANSPORT_RELAYS
{
bail!(
"You have reached the maximum number of relays ({}).",
MAX_TRANSPORT_RELAYS
)
}
{
bail!(
"You have reached the maximum number of relays ({}).",
MAX_TRANSPORT_RELAYS
)
}
let provider = match configure(self, param).await {
@@ -557,6 +538,7 @@ async fn get_configured_param(
.collect(),
imap_user: param.imap.user.clone(),
imap_password: param.imap.password.clone(),
imap_folder: Some(param.imap.folder.clone()).filter(|folder| !folder.is_empty()),
smtp: servers
.iter()
.filter_map(|params| {
@@ -649,10 +631,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
progress!(ctx, 900);
let is_configured = ctx.is_configured().await?;
if !is_configured {
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
}
if !ctx.get_config_bool(Config::FixIsChatmail).await? {
if imap_session.is_chatmail() {
ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
@@ -729,8 +707,7 @@ async fn get_autoconfig(
ctx,
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
&format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
"https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
),
&param.addr,
)
@@ -743,7 +720,7 @@ async fn get_autoconfig(
// Outlook uses always SSL but different domains (this comment describes the next two steps)
if let Ok(res) = outlk_autodiscover(
ctx,
format!("https://{}/autodiscover/autodiscover.xml", &param_domain),
format!("https://{param_domain}/autodiscover/autodiscover.xml"),
)
.await
{
@@ -753,10 +730,7 @@ async fn get_autoconfig(
if let Ok(res) = outlk_autodiscover(
ctx,
format!(
"https://autodiscover.{}/autodiscover/autodiscover.xml",
&param_domain
),
format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",),
)
.await
{
@@ -767,7 +741,7 @@ async fn get_autoconfig(
// always SSL for Thunderbird's database
if let Ok(res) = moz_autoconfigure(
ctx,
&format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
&format!("https://autoconfig.thunderbird.net/v1.1/{param_domain}"),
&param.addr,
)
.await
@@ -816,7 +790,7 @@ pub enum Error {
mod tests {
use super::*;
use crate::config::Config;
use crate::login_param::EnteredServerLoginParam;
use crate::login_param::EnteredImapLoginParam;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -835,7 +809,7 @@ mod tests {
let entered_param = EnteredLoginParam {
addr: "alice@example.org".to_string(),
imap: EnteredServerLoginParam {
imap: EnteredImapLoginParam {
user: "alice@example.net".to_string(),
password: "foobar".to_string(),
..Default::default()
+4 -29
View File
@@ -36,17 +36,6 @@ pub enum Blocked {
Request = 2,
}
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)]
pub enum ShowEmails {
Off = 0,
AcceptedContacts = 1,
#[default] // also change Config.ShowEmails props(default) on changes
All = 2,
}
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
@@ -199,7 +188,7 @@ pub const WORSE_IMAGE_BYTES: usize = 130_000;
// max. width/height and bytes of an avatar
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512;
pub(crate) const BALANCED_AVATAR_BYTES: usize = 60_000;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 256;
pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k.
// max. width/height of images scaled down because of being too huge
@@ -210,11 +199,6 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
/// usage by UIs.
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
// Key for the folder configuration version (see below).
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 5;
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
// `max_smtp_rcpt_to` in the provider db.
@@ -249,6 +233,9 @@ Here is what to do:
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
/// How many recent messages should be re-sent to a new broadcast member.
pub(crate) const N_MSGS_TO_NEW_BROADCAST_MEMBER: usize = 10;
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
@@ -264,18 +251,6 @@ mod tests {
assert_eq!(Chattype::OutBroadcast, Chattype::from_i32(160).unwrap());
}
#[test]
fn test_showemails_values() {
// values may be written to disk and must not change
assert_eq!(ShowEmails::All, ShowEmails::default());
assert_eq!(ShowEmails::Off, ShowEmails::from_i32(0).unwrap());
assert_eq!(
ShowEmails::AcceptedContacts,
ShowEmails::from_i32(1).unwrap()
);
assert_eq!(ShowEmails::All, ShowEmails::from_i32(2).unwrap());
}
#[test]
fn test_blocked_values() {
// values may be written to disk and must not change
+5 -4
View File
@@ -1182,7 +1182,9 @@ VALUES (?, ?, ?, ?, ?, ?)
let mut ret = Vec::new();
let flag_add_self = (listflags & constants::DC_GCL_ADD_SELF) != 0;
let flag_address = (listflags & constants::DC_GCL_ADDRESS) != 0;
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
let minimal_origin = if context.get_config_bool(Config::Bot).await?
|| query.is_some_and(may_be_valid_addr)
{
Origin::Unknown
} else {
Origin::IncomingReplyTo
@@ -1394,7 +1396,7 @@ WHERE addr=?
let Some(fingerprint_other) = contact.fingerprint() else {
return Ok(stock_str::encr_none(context));
};
let fingerprint_other = fingerprint_other.to_string();
let fingerprint_other = fingerprint_other.human_readable();
let stock_message = if contact.public_key(context).await?.is_some() {
stock_str::messages_are_e2ee(context)
@@ -1408,7 +1410,7 @@ WHERE addr=?
let fingerprint_self = load_self_public_key(context)
.await?
.dc_fingerprint()
.to_string();
.human_readable();
if addr < contact.addr {
cat_fingerprint(
&mut ret,
@@ -2066,7 +2068,6 @@ pub(crate) async fn mark_contact_id_as_verified(
Ok(())
}
#[expect(clippy::arithmetic_side_effects)]
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
}
+51 -2
View File
@@ -4,6 +4,7 @@ use super::*;
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote, sync};
#[test]
@@ -154,6 +155,50 @@ async fn test_get_contacts() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_search_contacts_from_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_chat_id = chat::create_group(alice, "").await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
tcm.exec_securejoin_qr(fiona, alice, &qr).await;
// Workaround for "member added" for fiona not sent to bob.
let gossip_period = alice.get_config_int(Config::GossipPeriod).await?;
SystemTime::shift(Duration::from_secs(gossip_period.try_into()?));
send_text_msg(alice, alice_chat_id, "hello".to_string()).await?;
let sent_msg = alice.pop_sent_msg().await;
bob.recv_msg(&sent_msg).await;
fiona.recv_msg(&sent_msg).await;
let contacts = Contact::get_all(bob, 0, None).await?;
let bob_alice_id = bob.add_or_lookup_contact_id(alice).await;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0], bob_alice_id);
let contacts = Contact::get_all(fiona, 0, None).await?;
let fiona_alice_id = fiona.add_or_lookup_contact_id(alice).await;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0], fiona_alice_id);
// Sending to the group adds new members to the contact list.
send_text_msg(bob, bob_chat_id, "hello".to_string()).await?;
fiona.recv_msg(&bob.pop_sent_msg().await).await;
let contacts = Contact::get_all(bob, 0, None).await?;
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
assert_eq!(contacts.len(), 2);
assert_eq!(contacts[0], bob_alice_id);
assert_eq!(contacts[1], bob_fiona_id);
let contacts = Contact::get_all(fiona, 0, None).await?;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0], fiona_alice_id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_is_self_addr() -> Result<()> {
let t = TestContext::new().await;
@@ -420,12 +465,16 @@ async fn test_delete() -> Result<()> {
Contact::delete(&alice, contact_id).await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert_eq!(contact.origin, Origin::Hidden);
// Hidden contacts are found when searching by email address
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
.len(),
0
1
);
// Hidden contacts are not found by a non-address query
assert_eq!(Contact::get_all(&alice, 0, Some("bob")).await?.len(), 0);
// Delete chat.
chat.get_id().delete(&alice).await?;
@@ -483,7 +532,7 @@ async fn test_delete_and_recreate_contact() -> Result<()> {
Contact::get_all(&t, 0, Some("bob@example.net"))
.await?
.len(),
0
1
);
let contact_id3 = t.add_or_lookup_contact_id(&bob).await;
+34 -78
View File
@@ -20,16 +20,16 @@ use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSI
use crate::contact::{Contact, ContactId};
use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::imap::{Imap, ServerMetadata};
use crate::key::self_fingerprint;
use crate::log::warn;
use crate::logged_debug_assert;
use crate::message::{self, MessageState, MsgId};
use crate::net::tls::TlsSessionStore;
use crate::net::tls::{SpkiHashStore, TlsSessionStore};
use crate::peer_channels::Iroh;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning};
use crate::scheduler::{ConnectivityStore, SchedulerState};
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -308,6 +308,13 @@ pub struct InnerContext {
/// TLS session resumption cache.
pub(crate) tls_session_store: TlsSessionStore,
/// Store for TLS SPKI hashes.
///
/// Used to remember public keys
/// of TLS certificates to accept them
/// even after they expire.
pub(crate) spki_hash_store: SpkiHashStore,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
@@ -511,6 +518,7 @@ impl Context {
push_subscriber,
push_subscribed: AtomicBool::new(false),
tls_session_store: TlsSessionStore::new(),
spki_hash_store: SpkiHashStore::new(),
iroh: Arc::new(RwLock::new(None)),
self_fingerprint: OnceLock::new(),
self_public_key: Mutex::new(None),
@@ -623,17 +631,10 @@ impl Context {
let mut session = connection.prepare(self).await?;
// Fetch IMAP folders.
// Inbox is fetched before Mvbox because fetching from Inbox
// may result in moving some messages to Mvbox.
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
if let Some((_folder_config, watch_folder)) =
convert_folder_meaning(self, folder_meaning).await?
{
connection
.fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
.await?;
}
}
let folder = connection.folder.clone();
connection
.fetch_move_delete(self, &mut session, &folder)
.await?;
// Update quota (to send warning if full) - but only check it once in a while.
// note: For now this only checks quota of primary transport,
@@ -644,7 +645,7 @@ impl Context {
DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT,
)
.await
&& let Err(err) = self.update_recent_quota(&mut session).await
&& let Err(err) = self.update_recent_quota(&mut session, &folder).await
{
warn!(self, "Failed to update quota: {err:#}.");
}
@@ -842,7 +843,7 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let all_self_addrs = self.get_all_self_addrs().await?.join(", ");
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
.await?
.into_iter()
@@ -884,23 +885,6 @@ impl Context {
Err(err) => format!("<key failure: {err}>"),
};
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?;
let folders_configured = self
.sql
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
.await?
.unwrap_or_default();
let configured_inbox_folder = self
.get_config(Config::ConfiguredInboxFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.get_config(Config::ConfiguredMvboxFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info();
// insert values
@@ -961,11 +945,7 @@ impl Context {
}
}
res.insert("secondary_addrs", secondary_addrs);
res.insert(
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert("all_self_addrs", all_self_addrs);
res.insert(
"who_can_call_me",
self.get_config_int(Config::WhoCanCallMe).await?.to_string(),
@@ -976,14 +956,6 @@ impl Context {
.await?
.to_string(),
);
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
res.insert(
constants::DC_FOLDERS_CONFIGURED_KEY,
folders_configured.to_string(),
);
res.insert("configured_inbox_folder", configured_inbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("bcc_self", bcc_self.to_string());
res.insert("sync_msgs", sync_msgs.to_string());
@@ -1001,12 +973,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"delete_server_after",
self.get_config_int(Config::DeleteServerAfter)
.await?
.to_string(),
);
res.insert(
"last_housekeeping",
self.get_config_int(Config::LastHousekeeping)
@@ -1019,24 +985,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"quota_exceeding",
self.get_config_int(Config::QuotaExceeding)
.await?
.to_string(),
);
res.insert(
"authserv_id_candidates",
self.get_config(Config::AuthservIdCandidates)
.await?
.unwrap_or_default(),
);
res.insert(
"sign_unencrypted",
self.get_config_int(Config::SignUnencrypted)
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
@@ -1142,10 +1090,17 @@ ORDER BY m.timestamp DESC,m.id DESC",
Ok(list)
}
/// Returns a list of messages with database ID higher than requested.
/// (deprecated) Returns a list of messages with database ID higher than requested.
///
/// Blocked contacts and chats are excluded,
/// but self-sent messages and contact requests are included in the results.
///
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
/// even if it is not fully downloaded yet.
/// The bot needs to wait for the message to be fully downloaded.
/// Since this is usually not the desired behavior,
/// bots should instead use the [`EventType::IncomingMsg`]
/// event for getting notified about new messages.
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
Some(s) => MsgId::new(s.parse()?),
@@ -1194,7 +1149,7 @@ ORDER BY m.timestamp DESC,m.id DESC",
Ok(list)
}
/// Returns a list of messages with database ID higher than last marked as seen.
/// (deprecated) Returns a list of messages with database ID higher than last marked as seen.
///
/// This function is supposed to be used by bot to request messages
/// that are not processed yet.
@@ -1204,6 +1159,13 @@ ORDER BY m.timestamp DESC,m.id DESC",
/// shortly after notification or notification is manually triggered
/// to interrupt waiting.
/// Notification may be manually triggered by calling [`Self::stop_io`].
///
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
/// even if it is not fully downloaded yet.
/// The bot needs to wait for the message to be fully downloaded.
/// Since this is usually not the desired behavior,
/// bots should instead use the #DC_EVENT_INCOMING_MSG / [`EventType::IncomingMsg`]
/// event for getting notified about new messages.
pub async fn wait_next_msgs(&self) -> Result<Vec<MsgId>> {
self.new_msgs_notify.notified().await;
let list = self.get_next_msgs().await?;
@@ -1283,12 +1245,6 @@ ORDER BY m.timestamp DESC,m.id DESC",
Ok(list)
}
/// Returns true if given folder name is the name of the "DeltaChat" folder.
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox.as_deref() == Some(folder_name))
}
pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
+5 -12
View File
@@ -284,7 +284,6 @@ async fn test_get_info_completeness() {
"send_security",
"server_flags",
"skip_start_messages",
"smtp_certificate_checks",
"proxy_url", // May contain passwords, don't leak it to the logs.
"socks5_enabled", // SOCKS5 options are deprecated.
"socks5_host",
@@ -603,10 +602,7 @@ async fn test_get_next_msgs() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
let alice = TestContext::new_alice().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
assert_eq!(alice.get_config(Config::Displayname).await?, None);
// Change the config circumventing the cache
// This simulates what the notification plugin on iOS might do
@@ -614,24 +610,21 @@ async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
alice
.sql
.execute(
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('displayname', 'Alice 2')",
(),
)
.await?;
// Alice's Delta Chat doesn't know about it yet:
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
assert_eq!(alice.get_config(Config::Displayname).await?, None);
// Starting IO will fail of course because no server settings are configured,
// but it should invalidate the caches:
alice.start_io().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("0".to_string())
alice.get_config(Config::Displayname).await?,
Some("Alice 2".to_string())
);
Ok(())
+6 -11
View File
@@ -15,12 +15,6 @@ use crate::{EventType, chatlist_events};
pub(crate) mod post_msg_metadata;
pub(crate) use post_msg_metadata::PostMsgMetadata;
/// If a message is downloaded only partially
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
/// the user might have no chance to actually download that message.
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
/// From this point onward outgoing messages are considered large
/// and get a Pre-Message, which announces the Post-Message.
/// This is only about sending so we can modify it any time.
@@ -173,9 +167,7 @@ pub(crate) async fn download_msg(
if msg_transport_id != transport_id {
return Ok(None);
}
session
.fetch_single_msg(context, &server_folder, server_uid, rfc724_mid)
.await?;
Box::pin(session.fetch_single_msg(context, &server_folder, server_uid, rfc724_mid)).await?;
Ok(Some(()))
}
@@ -204,8 +196,11 @@ impl Session {
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let (sender, receiver) = async_channel::unbounded();
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, sender)
.await?;
{
let _fetch_msgs_lock_guard = context.fetch_msgs_mutex.lock().await;
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, sender)
.await?;
}
if receiver.recv().await.is_err() {
bail!("Failed to fetch UID {uid}");
}
-10
View File
@@ -79,16 +79,6 @@ impl EncryptHelper {
Ok(ctext)
}
/// Signs the passed-in `mail` using the private key from `context`.
/// Returns the payload and the signature.
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
let mut buffer = Vec::new();
mail.clone().write_part(&mut buffer)?;
let signature = pgp::pk_calc_signature(buffer, &sign_key)?;
Ok(signature)
}
}
/// Ensures a private key exists for the configured user.
+8 -28
View File
@@ -23,16 +23,15 @@
//! ## Device settings
//!
//! In addition to per-chat ephemeral message setting, each device has
//! two global user-configured settings that complement per-chat
//! settings: `delete_device_after` and `delete_server_after`. These
//! settings are not synchronized among devices and apply to all
//! a global user-configured setting that complements per-chat
//! settings, `delete_device_after`.
//! This setting is not synchronized among devices and applies to all
//! messages known to the device, including messages sent or received
//! before configuring the setting.
//!
//! `delete_device_after` configures the maximum time device is
//! storing the messages locally. `delete_server_after` configures the
//! time after which device will delete the messages it knows about
//! from the server.
//! storing the messages locally,
//! but does not delete messages from the server.
//!
//! ## How messages are deleted
//!
@@ -60,9 +59,8 @@
//!
//! Server deletion happens by updating the `imap` table based on
//! the database entries which are expired either according to their
//! ephemeral message timers or global `delete_server_after` setting.
//! ephemeral message timers.
use std::cmp::max;
use std::collections::BTreeSet;
use std::fmt;
use std::num::ParseIntError;
@@ -78,7 +76,6 @@ use crate::chat::{ChatId, ChatIdBlocked, send_msg};
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
use crate::contact::ContactId;
use crate::context::Context;
use crate::download::MIN_DELETE_SERVER_AFTER;
use crate::events::EventType;
use crate::log::{LogExt, warn};
use crate::message::{Message, MessageState, MsgId, Viewtype};
@@ -651,23 +648,8 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
}
/// Schedules expired IMAP messages for deletion.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
let now = time();
let (threshold_timestamp, threshold_timestamp_extended) =
match context.get_config_delete_server_after().await? {
None => (0, 0),
Some(delete_server_after) => (
match delete_server_after {
// Guarantee immediate deletion.
0 => i64::MAX,
_ => now - delete_server_after,
},
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
),
};
context
.sql
.execute(
@@ -675,11 +657,9 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
SET target=''
WHERE rfc724_mid IN (
SELECT rfc724_mid FROM msgs
WHERE ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
WHERE ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?
)",
(threshold_timestamp, threshold_timestamp_extended, now),
(now,),
)
.await?;
-28
View File
@@ -455,7 +455,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
let uidvalidity = 12345;
for (id, timestamp, ephemeral_timestamp) in &[
(900, now - 2 * HOUR, 0),
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
(1010, now - 23 * HOUR, 0),
(1020, now - 21 * HOUR, 0),
(1030, now - 19 * HOUR, 0),
@@ -512,29 +511,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
0
);
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
.await?;
delete_expired_imap_messages(&t).await?;
test_marked_for_deletion(&t, 1000).await?;
MsgId::new(1000)
.update_download_state(&t, DownloadState::Available)
.await?;
t.sql
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
.await?;
delete_expired_imap_messages(&t).await?;
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
remove_uid(&t, 1000).await?;
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
.await?;
delete_expired_imap_messages(&t).await?;
test_marked_for_deletion(&t, 1010).await?;
t.sql
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
.await?;
MsgId::new(1010)
.update_download_state(&t, DownloadState::Available)
.await?;
@@ -547,10 +523,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
0
);
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
delete_expired_imap_messages(&t).await?;
test_marked_for_deletion(&t, 3000).await?;
Ok(())
}
+3 -7
View File
@@ -89,13 +89,9 @@ mod test_chatlist_events {
.get_matching(|evt| match evt {
EventType::ChatlistItemChanged {
chat_id: Some(ev_chat_id),
} => {
if ev_chat_id == &chat_id {
first_event_is_item.store(true, Ordering::Relaxed);
true
} else {
false
}
} if ev_chat_id == &chat_id => {
first_event_is_item.store(true, Ordering::Relaxed);
true
}
EventType::ChatlistChanged => true,
_ => false,
+1 -2
View File
@@ -234,8 +234,7 @@ pub enum EventType {
/// Location of one or more contact has changed.
///
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
/// If the locations of several contacts have been changed, this parameter is set to `None`.
LocationChanged(Option<ContactId>),
/// Inform about the configuration progress started by configure().
+17 -30
View File
@@ -86,8 +86,7 @@ impl HtmlMsgParser {
/// Function takes a raw mime-message string,
/// searches for the main-text part
/// and returns that as parser.html
#[expect(clippy::arithmetic_side_effects)]
pub async fn from_bytes<'a>(
pub fn from_bytes<'a>(
context: &Context,
rawmime: &'a [u8],
) -> Result<(Self, mailparse::ParsedMail<'a>)> {
@@ -99,14 +98,14 @@ impl HtmlMsgParser {
let parsedmail = mailparse::parse_mail(rawmime).context("Failed to parse mail")?;
parser.collect_texts_recursive(context, &parsedmail).await?;
parser.collect_texts_recursive(context, &parsedmail)?;
if parser.html.is_empty() {
if let Some(plain) = &parser.plain {
parser.html = plain.to_html();
}
} else {
parser.cid_to_data_recursive(context, &parsedmail).await?;
parser.cid_to_data_recursive(context, &parsedmail)?;
}
parser.html += &mem::take(&mut parser.msg_html);
Ok((parser, parsedmail))
@@ -120,8 +119,7 @@ impl HtmlMsgParser {
/// Usually, there is at most one plain-text and one HTML-text part,
/// multiple plain-text parts might be used for mailinglist-footers,
/// therefore we use the first one.
#[expect(clippy::arithmetic_side_effects)]
async fn collect_texts_recursive<'a>(
fn collect_texts_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
@@ -129,7 +127,7 @@ impl HtmlMsgParser {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
Box::pin(self.collect_texts_recursive(context, cur_data)).await?
self.collect_texts_recursive(context, cur_data)?
}
Ok(())
}
@@ -138,7 +136,7 @@ impl HtmlMsgParser {
if raw.is_empty() {
return Ok(());
}
let (parser, mail) = Box::pin(HtmlMsgParser::from_bytes(context, &raw)).await?;
let (parser, mail) = HtmlMsgParser::from_bytes(context, &raw)?;
if !parser.html.is_empty() {
let mut text = "\r\n\r\n".to_string();
for h in mail.headers {
@@ -201,7 +199,7 @@ impl HtmlMsgParser {
/// Replace cid:-protocol by the data:-protocol where appropriate.
/// This allows the final html-file to be self-contained.
async fn cid_to_data_recursive<'a>(
fn cid_to_data_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
@@ -209,7 +207,7 @@ impl HtmlMsgParser {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
Box::pin(self.cid_to_data_recursive(context, cur_data)).await?;
self.cid_to_data_recursive(context, cur_data)?;
}
Ok(())
}
@@ -271,7 +269,7 @@ impl MsgId {
let rawmime = rawmime?;
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, &rawmime).await {
match HtmlMsgParser::from_bytes(context, &rawmime) {
Err(err) => {
warn!(context, "get_html: parser error: {:#}", err);
Ok(None)
@@ -289,7 +287,6 @@ impl MsgId {
mod tests {
use super::*;
use crate::chat::{self, Chat, forward_msgs, save_msgs};
use crate::config::Config;
use crate::constants;
use crate::contact::ContactId;
use crate::message::{MessengerMessage, Viewtype};
@@ -300,7 +297,7 @@ mod tests {
async fn test_htmlparse_plain_unspecified() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
@@ -318,7 +315,7 @@ This message does not have Content-Type nor Subject.<br/>
async fn test_htmlparse_plain_iso88591() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
@@ -336,7 +333,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
async fn test_htmlparse_plain_flowed() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
assert!(parser.plain.unwrap().flowed);
assert_eq!(
parser.html,
@@ -358,7 +355,7 @@ and will be wrapped as usual.<br/>
async fn test_htmlparse_alt_plain() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
@@ -378,7 +375,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_htmlparse_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_html.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
// on windows, `\r\n` linends are returned from mimeparser,
// however, rust multiline-strings use just `\n`;
@@ -396,7 +393,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_htmlparse_alt_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
assert_eq!(
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
r##"<html>
@@ -410,7 +407,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_htmlparse_alt_plain_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
assert_eq!(
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
r##"<html>
@@ -434,7 +431,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(test.find("data:").is_none());
// parsing converts cid: to data:
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
assert!(parser.html.contains("<html>"));
assert!(!parser.html.contains("Content-Id:"));
assert!(parser.html.contains("data:image/jpeg;base64,/9j/4AAQ"));
@@ -557,13 +554,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_html_forwarding_encrypted() {
let mut tcm = TestContextManager::new();
// Alice receives a non-delta html-message
// (`ShowEmails=AcceptedContacts` lets Alice actually receive non-delta messages for known
// contacts, the contact is marked as known by creating a chat using `chat_with_contact()`)
let alice = &tcm.alice().await;
alice
.set_config(Config::ShowEmails, Some("1"))
.await
.unwrap();
let chat = alice
.create_chat_with_contact("", "sender@testrun.org")
.await;
@@ -581,10 +572,6 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// receive the message on another device
let alice = &tcm.alice().await;
alice
.set_config(Config::ShowEmails, Some("0"))
.await
.unwrap();
let msg = alice.recv_msg(&msg).await;
assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
assert_eq!(msg.get_from_id(), ContactId::SELF);
+79 -261
View File
@@ -27,13 +27,14 @@ use crate::calls::{
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{self, Blocked, DC_VERSION_STR};
use crate::constants::{Blocked, DC_VERSION_STR};
use crate::contact::ContactId;
use crate::context::Context;
use crate::ensure_and_debug_assert;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::log::{LogExt, warn};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
use crate::message::{self, Message, MessageState, MsgId};
use crate::mimeparser;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
@@ -91,6 +92,9 @@ pub(crate) struct Imap {
oauth2: bool,
/// Watched folder.
pub(crate) folder: String,
authentication_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
@@ -162,7 +166,6 @@ pub enum FolderMeaning {
/// Spam folder.
Spam,
Inbox,
Mvbox,
Trash,
/// Virtual folders.
@@ -174,19 +177,6 @@ pub enum FolderMeaning {
Virtual,
}
impl FolderMeaning {
pub fn to_config(self) -> Option<Config> {
match self {
FolderMeaning::Unknown => None,
FolderMeaning::Spam => None,
FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
FolderMeaning::Trash => None,
FolderMeaning::Virtual => None,
}
}
}
struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
inner: Peekable<T>,
}
@@ -263,6 +253,11 @@ impl Imap {
let addr = &param.addr;
let strict_tls = param.strict_tls(proxy_config.is_some());
let oauth2 = param.oauth2;
let folder = param
.imap_folder
.clone()
.unwrap_or_else(|| "INBOX".to_string());
ensure_and_debug_assert!(!folder.is_empty(), "Watched folder name cannot be empty");
let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
Ok(Imap {
transport_id,
@@ -273,6 +268,7 @@ impl Imap {
proxy_config,
strict_tls,
oauth2,
folder,
authentication_failed_once: false,
connectivity: Default::default(),
conn_last_try: UNIX_EPOCH,
@@ -490,22 +486,14 @@ impl Imap {
/// that folders are created and IMAP capabilities are determined.
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
let configuring = false;
let mut session = match self.connect(context, configuring).await {
let session = match self.connect(context, configuring).await {
Ok(session) => session,
Err(err) => {
self.connectivity.set_err(context, &err);
self.connectivity.set_err(context, format!("{err:#}"));
return Err(err);
}
};
let folders_configured = context
.sql
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
.await?;
if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
self.configure_folders(context, &mut session).await?;
}
Ok(session)
}
@@ -518,15 +506,15 @@ impl Imap {
context: &Context,
session: &mut Session,
watch_folder: &str,
folder_meaning: FolderMeaning,
) -> Result<()> {
ensure_and_debug_assert!(!watch_folder.is_empty(), "Watched folder cannot be empty");
if !context.sql.is_open().await {
// probably shutdown
bail!("IMAP operation attempted while it is torn down");
}
let msgs_fetched = self
.fetch_new_messages(context, session, watch_folder, folder_meaning)
.fetch_new_messages(context, session, watch_folder)
.await
.context("fetch_new_messages")?;
if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
@@ -554,19 +542,9 @@ impl Imap {
context: &Context,
session: &mut Session,
folder: &str,
folder_meaning: FolderMeaning,
) -> Result<bool> {
let transport_id = session.transport_id();
if should_ignore_folder(context, folder, folder_meaning).await? {
info!(
context,
"Transport {transport_id}: Not fetching from {folder:?}."
);
session.new_mail = false;
return Ok(false);
}
let folder_exists = session
.select_with_uidvalidity(context, folder)
.await
@@ -589,9 +567,8 @@ impl Imap {
let mut read_cnt = 0;
loop {
let (n, fetch_more) = self
.fetch_new_msg_batch(context, session, folder, folder_meaning)
.await?;
let (n, fetch_more) =
Box::pin(self.fetch_new_msg_batch(context, session, folder)).await?;
read_cnt += n;
if !fetch_more {
return Ok(read_cnt > 0);
@@ -606,7 +583,6 @@ impl Imap {
context: &Context,
session: &mut Session,
folder: &str,
folder_meaning: FolderMeaning,
) -> Result<(usize, bool)> {
let transport_id = self.transport_id;
let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
@@ -676,13 +652,7 @@ impl Imap {
info!(context, "Deleting locally deleted message {message_id}.");
}
let _target;
let target = if delete {
""
} else {
_target = target_folder(context, folder, folder_meaning, &headers).await?;
&_target
};
let target = if delete { "" } else { folder };
context
.sql
@@ -710,18 +680,9 @@ impl Imap {
// message, move it to the movebox and then download the second message before
// downloading the first one, if downloading from inbox before moving is allowed.
if folder == target
// Never download messages directly from the spam folder.
// If the sender is known, the message will be moved to the Inbox or Mvbox
// and then we download the message from there.
// Also see `spam_target_folder_cfg()`.
&& folder_meaning != FolderMeaning::Spam
&& prefetch_should_download(
context,
&headers,
&message_id,
fetch_response.flags(),
)
.await.context("prefetch_should_download")?
&& prefetch_should_download(context, &headers, &message_id, fetch_response.flags())
.await
.context("prefetch_should_download")?
{
if headers
.get_header_value(HeaderDef::ChatIsPostMessage)
@@ -730,10 +691,19 @@ impl Imap {
info!(context, "{message_id:?} is a post-message.");
available_post_msgs.push(message_id.clone());
if download_limit.is_none_or(|download_limit| size <= download_limit) {
download_later.push(message_id.clone());
let is_bot = context.get_config_bool(Config::Bot).await?;
if is_bot && download_limit.is_none_or(|download_limit| size <= download_limit)
{
uids_fetch.push(uid);
uid_message_ids.insert(uid, message_id);
} else {
if download_limit.is_none_or(|download_limit| size <= download_limit) {
// Download later after all the small messages are downloaded,
// so that large messages don't delay receiving small messages
download_later.push(message_id.clone());
}
largest_uid_skipped = Some(uid);
}
largest_uid_skipped = Some(uid);
} else {
info!(context, "{message_id:?} is not a post-message.");
if download_limit.is_none_or(|download_limit| size <= download_limit) {
@@ -1075,15 +1045,12 @@ impl Session {
if target.is_empty() {
self.delete_message_batch(context, &uid_set, rowid_set)
.await
.with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
.with_context(|| format!("cannot delete batch of messages {uid_set:?}"))?;
} else {
self.move_message_batch(context, &uid_set, rowid_set, &target)
.await
.with_context(|| {
format!(
"cannot move batch of messages {:?} to folder {:?}",
&uid_set, target
)
format!("cannot move batch of messages {uid_set:?} to folder {target:?}",)
})?;
}
}
@@ -1103,15 +1070,22 @@ impl Session {
return Ok(());
}
context
.sql
.execute(
"DELETE FROM imap_markseen WHERE id NOT IN (SELECT imap.id FROM imap)",
(),
)
.await?;
let transport_id = self.transport_id();
let rows = context
let mut rows = context
.sql
.query_map_vec(
"SELECT imap.id, uid, folder FROM imap, imap_markseen
WHERE imap.id = imap_markseen.id
AND imap.transport_id=?
AND target = folder
ORDER BY folder, uid",
AND target = folder",
(transport_id,),
|row| {
let rowid: i64 = row.get(0)?;
@@ -1122,6 +1096,16 @@ impl Session {
)
.await?;
// Number of SQL results is expected to be low as
// we usually don't have many messages to mark on IMAP at once.
// We are sorting outside of SQL to avoid SQLite constructing a query plan
// that scans the whole `imap` table. Scanning `imap_markseen` is fine
// as it should not have many items.
// If you change the SQL query, test it with `EXPLAIN QUERY PLAN`.
rows.sort_unstable_by(|(_rowid1, uid1, folder1), (_rowid2, uid2, folder2)| {
(folder1, uid1).cmp(&(folder2, uid2))
});
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
Err(err) => {
@@ -1297,12 +1281,14 @@ impl Session {
if request_uids.is_empty() {
return Ok(());
}
let is_chatmail = self.is_chatmail();
for (request_uids, set) in build_sequence_sets(&request_uids)? {
info!(context, "Starting UID FETCH of message set \"{}\".", set);
let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
format!("fetching messages {} from folder \"{}\"", &set, folder)
})?;
let mut fetch_responses = self
.uid_fetch(&set, BODY_FULL)
.await
.with_context(|| format!("fetching messages {set} from folder {folder:?}"))?;
// Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
// when we want to process other messages first.
@@ -1394,6 +1380,20 @@ impl Session {
"Passing message UID {} to receive_imf().", request_uid
);
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
// If the message is not needed anymore on the server, mark it for deletion:
if !context.get_config_bool(Config::BccSelf).await? && is_chatmail {
context
.sql
.execute(
"UPDATE imap SET target='' WHERE rfc724_mid=?",
(rfc724_mid,),
)
.await?;
context.scheduler.interrupt_inbox().await;
}
// If there was an error receiving the message, show a device message:
let received_msg = match res {
Err(err) => {
warn!(context, "receive_imf error: {err:#}.");
@@ -1645,13 +1645,8 @@ impl Session {
// Store new encrypted device token on the server
// even if it is the same as the old one.
if let Some(encrypted_device_token) = new_encrypted_device_token {
let folder = context
.get_config(Config::ConfiguredInboxFolder)
.await?
.context("INBOX is not configured")?;
self.run_command_and_check_ok(&format_setmetadata(
&folder,
"INBOX",
&encrypted_device_token,
))
.await
@@ -1696,117 +1691,6 @@ impl Session {
}
Ok(())
}
/// Attempts to configure mvbox.
///
/// Tries to find any folder examining `folders` in the order they go.
/// This method does not use LIST command to ensure that
/// configuration works even if mailbox lookup is forbidden via Access Control List (see
/// <https://datatracker.ietf.org/doc/html/rfc4314>).
///
/// Returns first found folder name.
async fn configure_mvbox<'a>(
&mut self,
context: &Context,
folders: &[&'a str],
) -> Result<Option<&'a str>> {
// Close currently selected folder if needed.
// We are going to select folders using low-level EXAMINE operations below.
self.maybe_close_folder(context).await?;
for folder in folders {
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
let res = self.examine(&folder).await;
if res.is_ok() {
info!(
context,
"MVBOX-folder {:?} successfully selected, using it.", &folder
);
self.close().await?;
// Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
// emails moved before that wouldn't be fetched but considered "old" instead.
let folder_exists = self.select_with_uidvalidity(context, folder).await?;
ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
return Ok(Some(folder));
}
}
Ok(None)
}
}
impl Imap {
pub(crate) async fn configure_folders(
&mut self,
context: &Context,
session: &mut Session,
) -> Result<()> {
let mut folders = session
.list(Some(""), Some("*"))
.await
.context("list_folders failed")?;
let mut delimiter = ".".to_string();
let mut delimiter_is_default = true;
let mut folder_configs = BTreeMap::new();
while let Some(folder) = folders.try_next().await? {
info!(context, "Scanning folder: {:?}", folder);
// Update the delimiter iff there is a different one, but only once.
if let Some(d) = folder.delimiter()
&& delimiter_is_default
&& !d.is_empty()
&& delimiter != d
{
delimiter = d.to_string();
delimiter_is_default = false;
}
let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
}
}
drop(folders);
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let fallback_folder = format!("INBOX{delimiter}DeltaChat");
let mvbox_folder = session
.configure_mvbox(context, &["DeltaChat", &fallback_folder])
.await
.context("failed to configure mvbox")?;
context
.set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
.await?;
if let Some(mvbox_folder) = mvbox_folder {
info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
context
.set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
.await?;
}
for (config, name) in folder_configs {
context.set_config_internal(config, Some(&name)).await?;
}
context
.sql
.set_raw_config_int(
constants::DC_FOLDERS_CONFIGURED_KEY,
constants::DC_FOLDERS_CONFIGURED_VERSION,
)
.await?;
info!(context, "FINISHED configuring IMAP-folders.");
Ok(())
}
}
impl Session {
@@ -1940,15 +1824,7 @@ async fn spam_target_folder_cfg(
return Ok(None);
}
if needs_move_to_mvbox(context, headers).await?
// If OnlyFetchMvbox is set, we don't want to move the message to
// the inbox where we wouldn't fetch it again:
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
{
Ok(Some(Config::ConfiguredMvboxFolder))
} else {
Ok(Some(Config::ConfiguredInboxFolder))
}
Ok(Some(Config::ConfiguredInboxFolder))
}
/// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if
@@ -1959,16 +1835,12 @@ pub async fn target_folder_cfg(
folder_meaning: FolderMeaning,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> {
if context.is_mvbox(folder).await? {
if folder == "DeltaChat" {
return Ok(None);
}
if folder_meaning == FolderMeaning::Spam {
spam_target_folder_cfg(context, headers).await
} else if folder_meaning == FolderMeaning::Inbox
&& needs_move_to_mvbox(context, headers).await?
{
Ok(Some(Config::ConfiguredMvboxFolder))
} else {
Ok(None)
}
@@ -1989,27 +1861,6 @@ pub async fn target_folder(
}
}
async fn needs_move_to_mvbox(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
) -> Result<bool> {
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
}
if has_chat_version {
Ok(true)
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
match parent.is_dc_message {
MessengerMessage::No => Ok(false),
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
}
} else {
Ok(false)
}
}
/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
// TODO: lots languages missing - maybe there is a list somewhere on other MUAs?
// however, if we fail to find out the sent-folder,
@@ -2163,7 +2014,7 @@ pub(crate) async fn prefetch_should_download(
return Ok(false);
}
let should_download = (!blocked_contact) || maybe_ndn;
let should_download = !blocked_contact || maybe_ndn;
Ok(should_download)
}
@@ -2340,21 +2191,6 @@ async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Resul
.unwrap_or(0))
}
/// Whether to ignore fetching messages from a folder.
///
/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
/// not explicitly watched should not be fetched.
async fn should_ignore_folder(
context: &Context,
folder: &str,
folder_meaning: FolderMeaning,
) -> Result<bool> {
if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
return Ok(false);
}
Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
}
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
@@ -2414,23 +2250,5 @@ impl std::fmt::Display for UidRange {
}
}
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
let mut res = vec![Config::ConfiguredInboxFolder];
if context.should_watch_mvbox().await? {
res.push(Config::ConfiguredMvboxFolder);
}
Ok(res)
}
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
let mut res = Vec::new();
for folder_config in get_watched_folder_configs(context).await? {
if let Some(folder) = context.get_config(folder_config).await? {
res.push(folder);
}
}
Ok(res)
}
#[cfg(test)]
mod imap_tests;
+8
View File
@@ -220,6 +220,8 @@ impl Client {
alpn(addr.port()),
logging_stream,
&context.tls_session_store,
&context.spki_hash_store,
&context.sql,
)
.await?;
let buffered_stream = BufWriter::new(tls_stream);
@@ -282,6 +284,8 @@ impl Client {
"",
tcp_stream,
&context.tls_session_store,
&context.spki_hash_store,
&context.sql,
)
.await
.context("STARTTLS upgrade failed")?;
@@ -310,6 +314,8 @@ impl Client {
alpn(port),
proxy_stream,
&context.tls_session_store,
&context.spki_hash_store,
&context.sql,
)
.await?;
let buffered_stream = BufWriter::new(tls_stream);
@@ -373,6 +379,8 @@ impl Client {
"",
proxy_stream,
&context.tls_session_store,
&context.spki_hash_store,
&context.sql,
)
.await
.context("STARTTLS upgrade failed")?;
+1 -5
View File
@@ -120,11 +120,7 @@ impl Session {
impl Imap {
/// Idle using polling.
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
watch_folder: String,
) -> Result<()> {
pub(crate) async fn fake_idle(&mut self, context: &Context, watch_folder: &str) -> Result<()> {
let fake_idle_start_time = tools::Time::now();
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
+22 -58
View File
@@ -100,23 +100,16 @@ fn test_build_sequence_sets() {
async fn check_target_folder_combination(
folder: &str,
mvbox_move: bool,
chat_msg: bool,
expected_destination: &str,
accepted_chat: bool,
outgoing: bool,
) -> Result<()> {
println!(
"Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}"
"Testing: For folder {folder}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}"
);
let t = TestContext::new_alice().await;
t.ctx
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
.await?;
t.ctx
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
.await?;
if accepted_chat {
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
@@ -161,64 +154,42 @@ async fn check_target_folder_combination(
assert_eq!(
expected,
actual.as_deref(),
"For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}: expected {expected:?}, got {actual:?}"
"For folder {folder}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}: expected {expected:?}, got {actual:?}"
);
Ok(())
}
// chat_msg means that the message was sent by Delta Chat
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
("INBOX", false, false, "INBOX"),
("INBOX", false, true, "INBOX"),
("INBOX", true, false, "INBOX"),
("INBOX", true, true, "DeltaChat"),
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
("Spam", false, true, "INBOX"),
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
("Spam", true, true, "DeltaChat"),
// The tuples are (folder, chat_msg, expected_destination)
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, &str)] = &[
("INBOX", false, "INBOX"),
("INBOX", true, "INBOX"),
("Spam", false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
("Spam", true, "INBOX"),
];
// These are the same as above, but non-chat messages in Spam stay in Spam
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
("INBOX", false, false, "INBOX"),
("INBOX", false, true, "INBOX"),
("INBOX", true, false, "INBOX"),
("INBOX", true, true, "DeltaChat"),
("Spam", false, false, "Spam"),
("Spam", false, true, "INBOX"),
("Spam", true, false, "Spam"),
("Spam", true, true, "DeltaChat"),
const COMBINATIONS_REQUEST: &[(&str, bool, &str)] = &[
("INBOX", false, "INBOX"),
("INBOX", true, "INBOX"),
("Spam", false, "Spam"),
("Spam", true, "INBOX"),
];
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_target_folder_incoming_accepted() -> Result<()> {
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
check_target_folder_combination(
folder,
*mvbox_move,
*chat_msg,
expected_destination,
true,
false,
)
.await?;
for (folder, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
check_target_folder_combination(folder, *chat_msg, expected_destination, true, false)
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_target_folder_incoming_request() -> Result<()> {
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
check_target_folder_combination(
folder,
*mvbox_move,
*chat_msg,
expected_destination,
false,
false,
)
.await?;
for (folder, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
check_target_folder_combination(folder, *chat_msg, expected_destination, false, false)
.await?;
}
Ok(())
}
@@ -226,16 +197,9 @@ async fn test_target_folder_incoming_request() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_target_folder_outgoing() -> Result<()> {
// Test outgoing emails
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
check_target_folder_combination(
folder,
*mvbox_move,
*chat_msg,
expected_destination,
true,
true,
)
.await?;
for (folder, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
check_target_folder_combination(folder, *chat_msg, expected_destination, true, true)
.await?;
}
Ok(())
}
+4 -4
View File
@@ -16,7 +16,7 @@ use crate::net::session::SessionStream;
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
/// not necessarily sent by Delta Chat.
/// - Chat-Is-Post-Message to skip it in background fetch or when it is > `DownloadLimit`.
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
const PREFETCH_FLAGS: &str = "(UID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
DATE \
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
@@ -124,7 +124,7 @@ impl Session {
}
/// Prefetch `n_uids` messages starting from `uid_next`. Returns a list of fetch results in the
/// order of ascending delivery time to the server (INTERNALDATE).
/// order of ascending UIDs.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn prefetch(
&mut self,
@@ -142,10 +142,10 @@ impl Session {
let mut msgs = BTreeMap::new();
while let Some(msg) = list.try_next().await? {
if let Some(msg_uid) = msg.uid {
msgs.insert((msg.internal_date(), msg_uid), msg);
msgs.insert(msg_uid, msg);
}
}
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
Ok(Vec::from_iter(msgs))
}
}
+20 -19
View File
@@ -979,21 +979,16 @@ mod tests {
context1.set_config(Config::BccSelf, None).await?;
// Check that the settings are displayed correctly.
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
context1.set_config_bool(Config::IsChatmail, true).await?;
assert_eq!(
context1.get_config(Config::BccSelf).await?,
Some("0".to_string())
);
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("1".to_string())
);
context1.set_config_bool(Config::IsChatmail, true).await?;
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, false);
context1.set_config_bool(Config::IsMuted, true).await?;
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, true);
assert_eq!(context1.get_config_delete_server_after().await?, Some(0));
imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
let _event = context1
.evtracker
@@ -1010,15 +1005,9 @@ mod tests {
assert!(context2.is_configured().await?);
assert!(context2.is_chatmail().await?);
for ctx in [context1, context2] {
assert_eq!(
ctx.get_config(Config::BccSelf).await?,
Some("1".to_string())
);
assert_eq!(
ctx.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
assert_eq!(ctx.get_config_delete_server_after().await?, None);
// BccSelf should be enabled automatically when exporting a backup
assert_eq!(ctx.get_config_bool(Config::BccSelf).await?, true);
assert_eq!(ctx.get_config_bool(Config::IsMuted).await?, true);
}
Ok(())
}
@@ -1137,4 +1126,16 @@ mod tests {
Ok(())
}
/// Tests importing a backup from Delta Chat 1.30.3 for Android (core v1.86.0).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_import_ancient_backup() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = &tcm.unconfigured().await;
let backup_path = Path::new("test-data/core-1.86.0-backup.tar");
imex(context, ImexMode::ImportBackup, backup_path, None).await?;
Ok(())
}
}
+6 -8
View File
@@ -427,18 +427,16 @@ mod tests {
let self_chat = ctx1.get_self_chat().await;
let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap();
assert_eq!(msgs.len(), 2);
let msgid = match msgs.first().unwrap() {
ChatItem::Message { msg_id } => msg_id,
_ => panic!("wrong chat item"),
let ChatItem::Message { msg_id } = msgs.first().unwrap() else {
panic!("wrong chat item");
};
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap();
let text = msg.get_text();
assert_eq!(text, "hi there");
let msgid = match msgs.get(1).unwrap() {
ChatItem::Message { msg_id } => msg_id,
_ => panic!("wrong chat item"),
let ChatItem::Message { msg_id } = msgs.get(1).unwrap() else {
panic!("wrong chat item");
};
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap();
let path = msg.get_file(&ctx1).unwrap();
assert_eq!(
+25 -21
View File
@@ -1,7 +1,7 @@
//! Cryptographic key module.
use std::collections::BTreeMap;
use std::fmt;
use std::fmt::{self, Write as _};
use std::io::Cursor;
use anyhow::{Context as _, Result, bail, ensure};
@@ -570,9 +570,11 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {
/// Creates new 160-bit (20 bytes) fingerprint.
/// Creates new fingerprint.
///
/// It is 160-bit (20 bytes) for v4 keys and 32 bytes for v6 keys.
pub fn new(v: Vec<u8>) -> Fingerprint {
debug_assert_eq!(v.len(), 20);
debug_assert!(v.len() == 20 || v.len() == 32);
Fingerprint(v)
}
@@ -583,6 +585,21 @@ impl Fingerprint {
pub fn hex(&self) -> String {
hex::encode_upper(&self.0)
}
/// Make a human-readable fingerprint.
pub fn human_readable(&self) -> String {
let mut f = String::new();
// Split key into chunks of 4 with space and newline at 20 chars
for (i, c) in self.hex().chars().enumerate() {
if i > 0 && i % 20 == 0 {
writeln!(&mut f).ok();
} else if i > 0 && i % 4 == 0 {
write!(&mut f, " ").ok();
}
write!(&mut f, "{c}").ok();
}
f
}
}
impl From<pgp::types::Fingerprint> for Fingerprint {
@@ -599,22 +616,6 @@ impl fmt::Debug for Fingerprint {
}
}
/// Make a human-readable fingerprint.
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Split key into chunks of 4 with space and newline at 20 chars
for (i, c) in self.hex().chars().enumerate() {
if i > 0 && i % 20 == 0 {
writeln!(f)?;
} else if i > 0 && i % 4 == 0 {
write!(f, " ")?;
}
write!(f, "{c}")?;
}
Ok(())
}
}
/// Parse a human-readable or otherwise formatted fingerprint.
impl std::str::FromStr for Fingerprint {
type Err = anyhow::Error;
@@ -626,7 +627,10 @@ impl std::str::FromStr for Fingerprint {
.filter(|&c| c.is_ascii_hexdigit())
.collect();
let v: Vec<u8> = hex::decode(&hex_repr)?;
ensure!(v.len() == 20, "wrong fingerprint length: {hex_repr}");
ensure!(
v.len() == 20 || v.len() == 32,
"wrong fingerprint length: {hex_repr}"
);
let fp = Fingerprint::new(v);
Ok(fp)
}
@@ -890,7 +894,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
1, 2, 4, 8, 16, 32, 64, 128, 255, 1, 2, 4, 8, 16, 32, 64, 128, 255, 19, 20,
]);
assert_eq!(
fp.to_string(),
fp.human_readable(),
"0102 0408 1020 4080 FF01\n0204 0810 2040 80FF 1314"
);
}
+1 -1
View File
@@ -17,6 +17,7 @@
clippy::cloned_instead_of_copied,
clippy::manual_is_variant_and
)]
#![cfg_attr(not(test), warn(clippy::large_futures))]
#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
@@ -100,7 +101,6 @@ mod update_helper;
pub mod webxdc;
#[macro_use]
mod dehtml;
mod authres;
pub mod color;
pub mod html;
pub mod net;
+68 -51
View File
@@ -264,15 +264,11 @@ impl Kml {
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
#[expect(clippy::arithmetic_side_effects)]
pub async fn send_locations_to_chat(
context: &Context,
chat_id: ChatId,
seconds: i64,
) -> Result<()> {
pub async fn send_to_chat(context: &Context, chat_id: ChatId, seconds: i64) -> Result<()> {
ensure!(seconds >= 0);
ensure!(!chat_id.is_special());
let now = time();
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?;
let is_sending_locations_before = is_sending_to_chat(context, chat_id).await?;
context
.sql
.execute(
@@ -305,35 +301,49 @@ pub async fn send_locations_to_chat(
Ok(())
}
/// Returns whether `chat_id` or any chat is sending locations.
///
/// If `chat_id` is `Some` only that chat is checked, otherwise returns `true` if any chat
/// is sending locations.
pub async fn is_sending_locations_to_chat(
context: &Context,
chat_id: Option<ChatId>,
) -> Result<bool> {
let exists = match chat_id {
Some(chat_id) => {
context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
(chat_id, time()),
)
.await?
}
None => {
context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
(time(),),
)
.await?
}
};
Ok(exists)
/// Returns whether any chat is sending locations.
pub async fn is_sending(context: &Context) -> Result<bool> {
context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?",
(time(),),
)
.await
}
/// Returns whether `chat_id` is sending locations.
pub async fn is_sending_to_chat(context: &Context, chat_id: ChatId) -> Result<bool> {
context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?",
(chat_id, time()),
)
.await
}
/// Returns a list of chats in which location streaming is enabled.
async fn get_chats_with_location_streaming(context: &Context) -> Result<Vec<ChatId>> {
context
.sql
.query_map_vec(
"SELECT id FROM chats WHERE locations_send_until>?",
(time(),),
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
},
)
.await
}
/// Stop sending locations in all chats.
pub async fn stop_sending(context: &Context) -> Result<()> {
for chat_id in get_chats_with_location_streaming(context).await? {
send_to_chat(context, chat_id, 0).await?;
}
Ok(())
}
/// Sets current location of the user device.
@@ -459,13 +469,6 @@ fn is_marker(txt: &str) -> bool {
}
}
/// Deletes all locations from the database.
pub async fn delete_all(context: &Context) -> Result<()> {
context.sql.execute("DELETE FROM locations;", ()).await?;
context.emit_location_changed(None).await?;
Ok(())
}
/// Deletes expired locations.
///
/// Only path locations are deleted.
@@ -495,7 +498,7 @@ pub(crate) async fn delete_expired(context: &Context, now: i64) -> Result<()> {
///
/// This function is used when a message is deleted
/// that has a corresponding `location_id`.
pub(crate) async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> {
pub(crate) async fn delete_poi(context: &Context, location_id: u32) -> Result<()> {
context
.sql
.execute(
@@ -507,7 +510,7 @@ pub(crate) async fn delete_poi_location(context: &Context, location_id: u32) ->
}
/// Deletes POI locations that don't have corresponding message anymore.
pub(crate) async fn delete_orphaned_poi_locations(context: &Context) -> Result<()> {
pub(crate) async fn delete_orphaned_poi(context: &Context) -> Result<()> {
context.sql.execute("
DELETE FROM locations
WHERE independent=1 AND id NOT IN
@@ -716,9 +719,9 @@ pub(crate) async fn save(
pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receiver<()>) {
loop {
let next_event = match maybe_send_locations(context).await {
let next_event = match maybe_send(context).await {
Err(err) => {
warn!(context, "maybe_send_locations failed: {:#}", err);
warn!(context, "location::maybe_send failed: {:#}", err);
Some(60) // Retry one minute later.
}
Ok(next_event) => next_event,
@@ -756,7 +759,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive
/// Returns number of seconds until the next time location streaming for some chat ends
/// automatically.
#[expect(clippy::arithmetic_side_effects)]
async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
async fn maybe_send(context: &Context) -> Result<Option<u64>> {
let mut next_event: Option<u64> = None;
let now = time();
@@ -868,7 +871,7 @@ mod tests {
use crate::config::Config;
use crate::message::MessageState;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use crate::test_utils::{ExpectedEvents, TestContext, TestContextManager};
use crate::tools::SystemTime;
#[test]
@@ -1051,7 +1054,7 @@ Content-Disposition: attachment; filename="location.kml"
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
send_to_chat(&alice, alice_chat.id, 1000).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
@@ -1100,10 +1103,13 @@ Content-Disposition: attachment; filename="location.kml"
.await?;
let alice_chat = alice.create_chat(bob).await;
// Bob needs the chat accepted so that "normal" messages from Alice trigger `IncomingMsg`.
// Location-only messages still must trigger `MsgsChanged`.
bob.create_chat(alice).await;
// Alice enables location streaming.
// Bob receives a message saying that Alice enabled location streaming.
send_locations_to_chat(alice, alice_chat.id, 60).await?;
send_to_chat(alice, alice_chat.id, 60).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
// Alice gets new location from GPS.
@@ -1113,8 +1119,19 @@ Content-Disposition: attachment; filename="location.kml"
// 10 seconds later location sending stream manages to send location.
SystemTime::shift(Duration::from_secs(10));
delete_expired(alice, time()).await?;
maybe_send_locations(alice).await?;
maybe_send(alice).await?;
bob.evtracker.clear_events();
bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
bob.evtracker
.get_matching_ex(
bob,
ExpectedEvents {
expected: |e| matches!(e, EventType::MsgsChanged { .. }),
unexpected: |e| matches!(e, EventType::IncomingMsg { .. }),
},
)
.await
.unwrap();
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);

Some files were not shown because too many files have changed in this diff Show More