Compare commits

...

35 Commits

Author SHA1 Message Date
iequidoo
974e32dd76 fix: Don't decrease member add/remove timestamps if they aren't far away in the future
We shouldn't decrease `add_timestamp` and `remove_timestamp` in the `chats_contacts` table normally,
even if remote changes arrive reordered. This particularly makes sense for ad-hoc groups (see
`chat::update_chat_contacts_table()` in `apply_group_changes()`) and in case if we join an encrypted
group which we were a member of before (see `chat::add_to_chat_contacts_table()` call).

Still, limit already stored timestamps in case local clock was in the future and is set back
now. But our clock may be slow, so limit stored timestamps with a remote timestamp if it's
bigger.

NB: `receive_imf::update_chats_contacts_timestamps()` already only increases timestamps, but it's
used only for handling of the "Chat-Group-Member-Timestamps" header, i.e. for encrypted groups.
2026-06-07 21:45:47 -03:00
iequidoo
c91608e9f1 fix: Don't send removal message to contact that hasn't been a chat member (#8298)
I.e. don't fail `remove_contact_from_chat()` for such a contact because there may be a race
condition with a remote removal of the contact done without trace, but don't send a removal message
and sync message in this case and don't emit a `ChatModified` event.

If a contact is already a past member, we still send a removal message to the chat, this is safe and
protects from lost removal messages, so there's no need to complicate the code in this case.
2026-06-07 13:23:16 -03:00
link2xt
207c2e6e4c feat: switch to aws-lc-rs cryptography provider for Rustls
aws-lc-rs is the default provider for Rustls.
We have been using ring as the default provider
to avoid duplicate dependency
because iroh supports only ring in 0.35.0,
but with version 1.0 it will be possible
to select other crypto providers
and we can switch to using more common default.

This change also enables "tls12" feature explicitly,
otherwise if there is no other dependency
requiring it, only TLS 1.3 will be supported
and we want to keep supporting TLS 1.2.
2026-06-06 09:56:31 +00:00
dependabot[bot]
c0705a8d92 chore(deps): bump taiki-e/install-action from 2.79.2 to 2.79.10
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.79.2 to 2.79.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](213ccc1a07...60ae4ce63c)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 19:55:34 +00:00
dependabot[bot]
18ce5a02cc chore(deps): bump EmbarkStudios/cargo-deny-action from 2.0.18 to 2.0.19
Bumps [EmbarkStudios/cargo-deny-action](https://github.com/embarkstudios/cargo-deny-action) from 2.0.18 to 2.0.19.
- [Release notes](https://github.com/embarkstudios/cargo-deny-action/releases)
- [Commits](6c8f9facfa...a531616d8c)

---
updated-dependencies:
- dependency-name: EmbarkStudios/cargo-deny-action
  dependency-version: 2.0.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 19:55:13 +00:00
dependabot[bot]
e87e269f98 chore(cargo): bump serde_json from 1.0.149 to 1.0.150
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.149 to 1.0.150.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 19:54:21 +00:00
dependabot[bot]
4bb557cf53 chore(cargo): bump log from 0.4.29 to 0.4.30
Bumps [log](https://github.com/rust-lang/log) from 0.4.29 to 0.4.30.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.30)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 19:54:02 +00:00
dependabot[bot]
9b4503e3f5 chore(cargo): bump tokio from 1.52.1 to 1.52.3
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.52.1 to 1.52.3.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.1...tokio-1.52.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 19:53:16 +00:00
dependabot[bot]
4428382433 chore(cargo): bump pin-project from 1.1.11 to 1.1.13
Bumps [pin-project](https://github.com/taiki-e/pin-project) from 1.1.11 to 1.1.13.
- [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.11...v1.1.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 15:24:20 +00:00
Hocuri
8c56b63f21 feat: Add number_of_keys to statistics (#8297)
As of 3b29469102, all public keys except
for the currently-used one are ignored. But Delta Chat still tries to
use all _private_ keys for decryption.

This PR adds the information how many keys there are in the database. If
there is more than one, we know that the user imported their own key
back when that was possible. This info is interesting both for ourselves
and for the Cispa researchers that want to write a paper about Delta
Chat.

An alternative to this PR would be to make `key_create_timestamps`
actually contain the full list of key creation timestamps. I'm not sure
if that's worth the effort, though.

Follow-up to #8293
2026-06-03 12:36:31 +02:00
Hocuri
50e83f2072 feat: Add cryptography-related statistics (#8293)
This adds `number_of_transports`, `key_version`, `key_algorithm`,
`pubkey_size`.
2026-06-02 10:26:53 +02:00
link2xt
4a94a34c6d chore: update preloaded DNS cache 2026-06-01 11:29:25 +00:00
Hocuri
bd6c9908e4 fix: Update the channel title after joining if the QR code included a wrong title (#8260)
Fix https://github.com/chatmail/core/issues/8250. Not sure why anyone
would tamper with the link, but maybe the name gets ellipsized because
it is very long, or maybe we decide to ellipsize more aggressively in
the future.

The fix is easy enough, just set the GroupNameTimestamp when creating
the channel, so that `HeaderDef::ChatGroupNameTimestamp` gets set when
sending a message and the logic in
`apply_chat_name_avatar_and_description_changes()` recognizes the
incoming name's timestamp as newer than `chat_group_name_timestamp`
(which is 0 right after joining a chat).

The test is somewhat hacky and depends on the title being the last
parameter in the invite code URL, but this is fine; if we ever change
that, then the test will fail and we need to modify it.

This also removes some wrong doc comments.

Since groups are unpromoted until sending a first message, the same fix isn't needed for groups.
2026-06-01 08:48:01 +00:00
iequidoo
dd5ec9621b feat: Add IMAP folder to Context::get_info() (#8285)
Note that the default value "INBOX" isn't shown however.
2026-05-31 19:59:31 -03:00
link2xt
92d522473f refactor: remove support for building "source" packages for deltachat-rpc-server
This hack was needed to "build" Android packages
by repackaging Linux packages on-the-fly,
but PyPI has since added Android support
and we are using it directly.
2026-05-31 08:45:39 +00:00
iequidoo
049fd4f355 refactor: Remove misplaced comment
It was added in ff99e1997 "inital commit" and even there it looks misplaced.
2026-05-29 15:39:15 -03:00
link2xt
7ea637c930 chore: bump version to 2.51.0-dev 2026-05-29 18:32:58 +02:00
link2xt
3a1b0d4679 chore(release): prepare for 2.51.0 2026-05-29 18:05:43 +02:00
WofWca
7a60c79301 feat: message info: add database msg ID
We have recently removed it from the normal "message info" dialog
at least on Delta Chat Desktop, so without this one can't easily find
the ID of a particular message,
which is useful for debugging.
2026-05-29 15:33:14 +04:00
link2xt
0bc7849e8a build(nix): fix downloads from crates.io in nix builds
crates.io recently started rejecting requests to https://crates.io/api/v1/crates
made by Nix: https://github.com/rust-lang/crates.io/issues/13482
This resulted in failing to download any dependencies
from crates.io with 403 error during Rust package builds.
The problem was solved in nixpkgs by switching
to CDN URL https://static.crates.io/crates
in https://github.com/NixOS/nixpkgs/pull/524985

As of writing this on 2026-05-29
the fix is on "master" branch of nixpkgs,
but not on "nixos-unstable" or "nixpkgs-unstable"
branches yet according to https://nixpk.gs/pr-tracker.html?pr=524985
so I have switched nixpkgs to "master" branch to get the problem fixed.

naersk needs a similar fix. I opened a PR for it
https://github.com/nix-community/naersk/pull/391
but because the fix is not merged yet,
switched to my PR branch in flake.nix.

Updating nixpkgs required some minor changes,
e.g. sphinx_rtd_theme has been renamed to sphinx-rtd-theme,
pthreads in Windows builds had to be moved to buildInputs to fix
"Refusing to evaluate package 'mingw_w64-pthreads-13.0.0'
in /nix/store/f78lkqnk63pd0kf52zf2wcx35p1nnalr-source/pkgs/os-specific/windows/mingw-w64/headers.nix:35
because it is not available on the requested hostPlatform"
2026-05-29 10:16:16 +00:00
link2xt
dfea8b0134 test: test encrypted messages in test_delete_expired_imap_messages 2026-05-29 08:37:21 +00:00
link2xt
376c819374 feat: immediately remove all encrypted messages from the server in single-device mode 2026-05-29 08:37:21 +00:00
link2xt
02827406f3 docs: fix reference in delete_expired_imap_messages comment
It should refer to delete_expired_messages,
not to itself.
2026-05-29 08:37:21 +00:00
link2xt
9d9f61d9eb refactor: make should_delete_all_downloaded_messages non-async
Pass bcc_self into it as a boolean.
2026-05-29 08:37:21 +00:00
link2xt
a2816d7bd3 test: test bcc_self in test_delete_expired_imap_messages 2026-05-29 08:37:21 +00:00
iequidoo
9719aa1415 fix: Don't make message OutDelivered after successful resending to new broadcast member
Follow-up to 970222f. We shouldn't update the message state to `OutDelivered` if message sending
failed before and the message was re-sent successfully only to a new broadcast member.
2026-05-29 00:35:46 -03:00
Tom Niget
5733a783fb feat: follow certificate check parameter in autoconfig 2026-05-28 20:18:40 +00:00
holger krekel
c26b6a017c fix: syntax error in older migration 2026-05-27 06:20:50 +00:00
dependabot[bot]
bb6a478430 chore(deps): bump taiki-e/install-action from 2.78.1 to 2.79.2
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.78.1 to 2.79.2.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](184183c240...213ccc1a07)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 05:27:42 +00:00
link2xt
480248a168 refactor: remove pre_encrypt_mime_hook
It is only used in a single test
and the test is more reliable by having a blob .eml
that will not be accidentally modified
by mimefactory changes.
2026-05-26 08:10:51 +00:00
dependabot[bot]
627f98915d chore(deps): bump zizmorcore/zizmor-action from 0.5.3 to 0.5.6
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.3 to 0.5.6.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](b1d7e1fb5d...5f14fd08f7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 05:06:42 +00:00
link2xt
24848c0265 fix: do not fail to receive post-message with status updates for deleted webxdc
receive_imf should not fail in this case,
but trash the post-message and updates.

The problem with previously used message::rfc724_mid_exists check
was that rfc724_mid_exists may return a trashed message.
2026-05-23 21:57:58 +00:00
link2xt
98123afb62 test: test IPv6 addresses in HTTP(S) proxies 2026-05-23 21:49:37 +00:00
link2xt
16b65c59bf fix: do not try to resolve proxy IPv6 addresses in square brackets
"[::1]" is not a hostname, but an IPv6 address,
and should be passed as "::1" to tokio::net functions.

Otherwise connecting to proxy addresses such as `socks5://[::1]:9150`
fails by trying to resolve IPv6 address with DNS.
2026-05-23 07:01:05 +00:00
link2xt
bafb5f71ad chore: bump version to 2.51.0-dev 2026-05-22 21:41:03 +02:00
50 changed files with 953 additions and 532 deletions

View File

@@ -62,7 +62,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@6c8f9facfa5047ec02d8485b6bf52b587b7777d1 - uses: EmbarkStudios/cargo-deny-action@a531616d8ce3b9177443e48a1159bc945a099823
with: with:
arguments: --workspace --all-features --locked arguments: --workspace --all-features --locked
command: check command: check
@@ -146,7 +146,7 @@ jobs:
cache-bin: false cache-bin: false
- name: Install nextest - name: Install nextest
uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 uses: taiki-e/install-action@60ae4ce63c7aeb6e96d7f572c1ec7fafbb17ca80
with: with:
tool: nextest tool: nextest

View File

@@ -63,7 +63,6 @@ jobs:
- deltachat-rpc-server-armv7l-linux-wheel - deltachat-rpc-server-armv7l-linux-wheel
- deltachat-rpc-server-i686-linux - deltachat-rpc-server-i686-linux
- deltachat-rpc-server-i686-linux-wheel - deltachat-rpc-server-i686-linux-wheel
- deltachat-rpc-server-source
- deltachat-rpc-server-win32 - deltachat-rpc-server-win32
- deltachat-rpc-server-win32-wheel - deltachat-rpc-server-win32-wheel
- deltachat-rpc-server-win64 - deltachat-rpc-server-win64

View File

@@ -23,4 +23,4 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6

View File

@@ -1,5 +1,44 @@
# Changelog # Changelog
## [2.51.0] - 2026-05-29
### Features / Changes
- Follow certificate check parameter in autoconfig.
- Immediately remove all encrypted messages from the server in single-device mode.
### Fixes
- Fix syntax error in `only_fetch_mvbox` migration 150 resulting in failure to upgrade for `only_fetch_mvbox` users.
- Do not try to resolve proxy IPv6 addresses in square brackets.
- Do not fail to receive post-message with status updates for deleted webxdc.
- Don't make message `OutDelivered` after successful resending to new broadcast member.
### Build system
- nix: fix downloads from crates.io in nix builds.
### Documentation
- Fix reference in `delete_expired_imap_messages` comment.
### Refactor
- Remove `pre_encrypt_mime_hook`.
- Make `should_delete_all_downloaded_messages` non-async.
### Tests
- Test IPv6 addresses in HTTP(S) proxies.
- Test `bcc_self` in `test_delete_expired_imap_messages`.
- Test encrypted messages in `test_delete_expired_imap_messages`.
### Miscellaneous Tasks
- Bump version to 2.51.0-dev.
- deps: bump zizmorcore/zizmor-action from 0.5.3 to 0.5.6.
- deps: bump taiki-e/install-action from 2.78.1 to 2.79.2.
## [2.50.0] - 2026-05-22 ## [2.50.0] - 2026-05-22
### API-Changes ### API-Changes
@@ -8257,3 +8296,4 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.48.0]: https://github.com/chatmail/core/compare/v2.47.0..v2.48.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 [2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
[2.50.0]: https://github.com/chatmail/core/compare/v2.49.0..v2.50.0 [2.50.0]: https://github.com/chatmail/core/compare/v2.49.0..v2.50.0
[2.51.0]: https://github.com/chatmail/core/compare/v2.50.0..v2.51.0

112
Cargo.lock generated
View File

@@ -391,6 +391,28 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "aws-lc-rs"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]] [[package]]
name = "backon" name = "backon"
version = "1.5.0" version = "1.5.0"
@@ -763,10 +785,13 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.14" version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [ dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex", "shlex",
] ]
@@ -920,6 +945,15 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "cobs" name = "cobs"
version = "0.2.3" version = "0.2.3"
@@ -1316,7 +1350,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat" name = "deltachat"
version = "2.50.0" version = "2.51.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"astral-tokio-tar", "astral-tokio-tar",
@@ -1425,7 +1459,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat-jsonrpc" name = "deltachat-jsonrpc"
version = "2.50.0" version = "2.51.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-channel 2.5.0", "async-channel 2.5.0",
@@ -1446,7 +1480,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat-repl" name = "deltachat-repl"
version = "2.50.0" version = "2.51.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"deltachat", "deltachat",
@@ -1462,7 +1496,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat-rpc-server" name = "deltachat-rpc-server"
version = "2.50.0" version = "2.51.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"deltachat", "deltachat",
@@ -1491,7 +1525,7 @@ dependencies = [
[[package]] [[package]]
name = "deltachat_ffi" name = "deltachat_ffi"
version = "2.50.0" version = "2.51.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"deltachat", "deltachat",
@@ -1676,7 +1710,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.61.1", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1726,6 +1760,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.18" version = "1.0.18"
@@ -2051,6 +2091,12 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]] [[package]]
name = "fixedbitset" name = "fixedbitset"
version = "0.5.7" version = "0.5.7"
@@ -2108,6 +2154,12 @@ dependencies = [
name = "format-flowed" name = "format-flowed"
version = "1.0.0" version = "1.0.0"
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]] [[package]]
name = "funty" name = "funty"
version = "2.0.0" version = "2.0.0"
@@ -2681,7 +2733,7 @@ dependencies = [
"hyper", "hyper",
"libc", "libc",
"pin-project-lite", "pin-project-lite",
"socket2 0.6.3", "socket2 0.5.9",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -3234,6 +3286,16 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.3",
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.77" version = "0.3.77"
@@ -3374,9 +3436,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
[[package]] [[package]]
name = "loom" name = "loom"
@@ -3818,7 +3880,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.1", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -4303,18 +4365,18 @@ dependencies = [
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.11" version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.1.11" version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5219,7 +5281,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.12.1", "linux-raw-sys 0.12.1",
"windows-sys 0.61.1", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -5228,6 +5290,7 @@ version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [ dependencies = [
"aws-lc-rs",
"log", "log",
"once_cell", "once_cell",
"ring", "ring",
@@ -5273,6 +5336,7 @@ version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [ dependencies = [
"aws-lc-rs",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
"untrusted", "untrusted",
@@ -5517,9 +5581,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -5695,9 +5759,9 @@ dependencies = [
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
@@ -6083,7 +6147,7 @@ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix 1.1.4", "rustix 1.1.4",
"windows-sys 0.61.1", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6231,9 +6295,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.1" version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat" name = "deltachat"
version = "2.50.0" version = "2.51.0-dev"
edition = "2024" edition = "2024"
license = "MPL-2.0" license = "MPL-2.0"
rust-version = "1.89" rust-version = "1.89"
@@ -101,7 +101,7 @@ tagger = "4.3.4"
textwrap = "0.16.2" textwrap = "0.16.2"
thiserror = { workspace = true } thiserror = { workspace = true }
tokio-io-timeout = "1.2.1" tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false } tokio-rustls = { version = "0.26.2", default-features = false, features = ["aws-lc-rs", "tls12"] }
tokio-stream = { version = "0.1.17", features = ["fs"] } tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.6.2", default-features = false } astral-tokio-tar = { version = "0.6.2", default-features = false }
tokio-util = { workspace = true } tokio-util = { workspace = true }

View File

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

View File

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

View File

@@ -1106,9 +1106,6 @@ impl CommandApi {
/// because the word "channel" already appears a lot in the code, /// because the word "channel" already appears a lot in the code,
/// which would make it hard to grep for it. /// which would make it hard to grep for it.
/// ///
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
/// see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
///
/// Returns the created chat's id. /// Returns the created chat's id.
async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result<u32> { async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?; let ctx = self.get_context(account_id).await?;

View File

@@ -54,5 +54,5 @@
}, },
"type": "module", "type": "module",
"types": "dist/deltachat.d.ts", "types": "dist/deltachat.d.ts",
"version": "2.50.0" "version": "2.51.0-dev"
} }

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "deltachat-rpc-client" name = "deltachat-rpc-client"
version = "2.50.0" version = "2.51.0-dev"
license = "MPL-2.0" license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface" description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [ classifiers = [

View File

@@ -340,9 +340,6 @@ class Account:
because the word "channel" already appears a lot in the code, because the word "channel" already appears a lot in the code,
which would make it hard to grep for it. which would make it hard to grep for it.
After creation, the chat contains no recipients and is in _unpromoted_ state;
see `create_group()` for more information on the unpromoted state.
Returns the created chat. Returns the created chat.
""" """
return Chat(self, self._rpc.create_broadcast(self.id, name)) return Chat(self, self._rpc.create_broadcast(self.id, name))

View File

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

View File

@@ -15,5 +15,5 @@
}, },
"type": "module", "type": "module",
"types": "index.d.ts", "types": "index.d.ts",
"version": "2.50.0" "version": "2.51.0-dev"
} }

151
flake.lock generated
View File

@@ -3,15 +3,19 @@
"android": { "android": {
"inputs": { "inputs": {
"devshell": "devshell", "devshell": "devshell",
"flake-utils": "flake-utils", "flake-utils": [
"nixpkgs": "nixpkgs" "flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1731356359, "lastModified": 1779918845,
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=", "narHash": "sha256-FbpOOBg15L7X6NWWmTKbSdccnH59Jq53wWmAO37d2Q8=",
"owner": "tadfisher", "owner": "tadfisher",
"repo": "android-nixpkgs", "repo": "android-nixpkgs",
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a", "rev": "105c093afc8c8fbeea98f8e398403f93043eba17",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -28,11 +32,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1728330715, "lastModified": 1768818222,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=", "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef", "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -43,15 +47,17 @@
}, },
"fenix": { "fenix": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs_2", "nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1763361733, "lastModified": 1779876442,
"narHash": "sha256-ka7dpwH3HIXCyD2wl5F7cPLeRbqZoY2ullALsvxdPt8=", "narHash": "sha256-O25HomVNmdROO13PEQ3Ran8Hq5EsyLmVn8Gb8JvJtJE=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "6c8d48e3b0ae371b19ac1485744687b788e80193", "rev": "2eff81fc84390a35e1565395ae945d9394856824",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -65,29 +71,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1726560853, "lastModified": 1731533236,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -98,29 +86,35 @@
}, },
"naersk": { "naersk": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs_3" "fenix": [
"fenix"
],
"nixpkgs": [
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1721727458, "lastModified": 1779912356,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", "narHash": "sha256-yj5O6vmAj+OfhTQMiUwhmQRP0HAII3BxEI6zuY6h/5k=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", "rev": "33eaf5c72a67db15073322d26cd342c443556214",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "nix-community",
"ref": "pull/391/head",
"repo": "naersk", "repo": "naersk",
"type": "github" "type": "github"
} }
}, },
"nix-filter": { "nix-filter": {
"locked": { "locked": {
"lastModified": 1730207686, "lastModified": 1757882181,
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=", "narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
"owner": "numtide", "owner": "numtide",
"repo": "nix-filter", "repo": "nix-filter",
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4", "rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -131,60 +125,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1731139594, "lastModified": 1779931091,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", "narHash": "sha256-gc8NEz7a++7OQPGvMv+zIjXCec1PO38XRXZRa3m97ew=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", "rev": "3052ddf0614791c1869384a868248be5607a309f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "nixos-unstable", "ref": "master",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 0,
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -193,20 +143,20 @@
"inputs": { "inputs": {
"android": "android", "android": "android",
"fenix": "fenix", "fenix": "fenix",
"flake-utils": "flake-utils_2", "flake-utils": "flake-utils",
"naersk": "naersk", "naersk": "naersk",
"nix-filter": "nix-filter", "nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_4" "nixpkgs": "nixpkgs"
} }
}, },
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1762860488, "lastModified": 1779827300,
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=", "narHash": "sha256-J6pHxKoZzWCrAvOVInwBcYYWix/NWwM10Ad+i29Qc5s=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763", "rev": "c3af07ad84d68adc5e652e86f0c20009caa29014",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -230,21 +180,6 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@@ -2,11 +2,16 @@
description = "Chatmail core"; description = "Chatmail core";
inputs = { inputs = {
fenix.url = "github:nix-community/fenix"; fenix.url = "github:nix-community/fenix";
fenix.inputs.nixpkgs.follows = "nixpkgs";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk"; naersk.url = "github:nix-community/naersk/pull/391/head";
naersk.inputs.nixpkgs.follows = "nixpkgs";
naersk.inputs.fenix.follows = "fenix";
nix-filter.url = "github:numtide/nix-filter"; nix-filter.url = "github:numtide/nix-filter";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/master";
android.url = "github:tadfisher/android-nixpkgs"; android.url = "github:tadfisher/android-nixpkgs";
android.inputs.nixpkgs.follows = "nixpkgs";
android.inputs.flake-utils.follows = "flake-utils";
}; };
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }: outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
@@ -133,6 +138,8 @@
]; ];
depsBuildBuild = [ depsBuildBuild = [
pkgsWin64.stdenv.cc pkgsWin64.stdenv.cc
];
buildInputs = [
pkgsWin64.windows.pthreads pkgsWin64.windows.pthreads
]; ];
auditable = false; # Avoid cargo-auditable failures. auditable = false; # Avoid cargo-auditable failures.
@@ -143,6 +150,8 @@
CARGO_BUILD_RUSTFLAGS = [ CARGO_BUILD_RUSTFLAGS = [
"-C" "-C"
"linker=${TARGET_CC}" "linker=${TARGET_CC}"
"-L"
"native=${pkgsWin64.windows.pthreads}/lib"
]; ];
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc"; CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
@@ -180,7 +189,8 @@
}; };
})).overrideAttrs (oldAttr: { })).overrideAttrs (oldAttr: {
configureFlags = oldAttr.configureFlags ++ [ configureFlags = oldAttr.configureFlags ++ [
"--disable-sjlj-exceptions --with-dwarf2" "--disable-sjlj-exceptions"
"--with-dwarf2"
]; ];
}) })
); );
@@ -196,6 +206,8 @@
]; ];
depsBuildBuild = [ depsBuildBuild = [
winCC winCC
];
buildInputs = [
pkgsWin32.windows.pthreads pkgsWin32.windows.pthreads
]; ];
auditable = false; # Avoid cargo-auditable failures. auditable = false; # Avoid cargo-auditable failures.
@@ -206,6 +218,8 @@
CARGO_BUILD_RUSTFLAGS = [ CARGO_BUILD_RUSTFLAGS = [
"-C" "-C"
"linker=${TARGET_CC}" "linker=${TARGET_CC}"
"-L"
"native=${pkgsWin32.windows.pthreads}/lib"
]; ];
CC = "${winCC}/bin/${winCC.targetPrefix}cc"; CC = "${winCC}/bin/${winCC.targetPrefix}cc";
@@ -504,22 +518,6 @@
''; '';
}; };
# Source package for deltachat-rpc-server.
# Fake package that downloads Linux version,
# needed to install deltachat-rpc-server on Android with `pip`.
deltachat-rpc-server-source =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-source";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat_rpc_server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-client = deltachat-rpc-client =
pkgs.python3Packages.buildPythonPackage { pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client"; pname = "deltachat-rpc-client";
@@ -562,7 +560,7 @@
deltachat-python deltachat-python
deltachat-rpc-client deltachat-rpc-client
pkgs.python3Packages.breathe pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme pkgs.python3Packages.sphinx-rtd-theme
]; ];
nativeBuildInputs = [ pkgs.sphinx ]; nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html''; buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "deltachat" name = "deltachat"
version = "2.50.0" version = "2.51.0-dev"
license = "MPL-2.0" license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat" description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst" readme = "README.rst"

View File

@@ -1 +1 @@
2026-05-22 2026-05-29

View File

@@ -20,86 +20,6 @@ Description-Content-Type: text/markdown
""" """
def build_source_package(version, filename):
with tarfile.open(filename, "w:gz") as pkg:
def pack(name, contents):
contents = contents.encode()
tar_info = tarfile.TarInfo(f"deltachat_rpc_server-{version}/{name}")
tar_info.mode = 0o644
tar_info.size = len(contents)
pkg.addfile(tar_info, BytesIO(contents))
pack("PKG-INFO", metadata_contents(version))
pack(
"pyproject.toml",
f"""[build-system]
requires = ["setuptools==68.2.2", "pip"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-server"
version = "{version}"
[project.scripts]
deltachat-rpc-server = "deltachat_rpc_server:main"
""",
)
pack(
"setup.py",
f"""
import sys
from setuptools import setup, find_packages
from distutils.cmd import Command
from setuptools.command.install import install
from setuptools.command.build import build
import subprocess
import platform
import tempfile
from zipfile import ZipFile
from pathlib import Path
import shutil
class BuildCommand(build):
def run(self):
tmpdir = tempfile.mkdtemp()
subprocess.run(
[
sys.executable,
"-m",
"pip",
"download",
"--no-input",
"--timeout",
"1000",
"--platform",
"musllinux_1_1_" + platform.machine(),
"--only-binary=:all:",
"deltachat-rpc-server=={version}",
],
cwd=tmpdir,
)
wheel_path = next(Path(tmpdir).glob("*.whl"))
with ZipFile(wheel_path, "r") as wheel:
exe_path = wheel.extract("deltachat_rpc_server/deltachat-rpc-server", "src")
Path(exe_path).chmod(0o700)
wheel.extract("deltachat_rpc_server/__init__.py", "src")
shutil.rmtree(tmpdir)
return super().run()
setup(
cmdclass={{"build": BuildCommand}},
package_data={{"deltachat_rpc_server": ["deltachat-rpc-server"]}},
)
""",
)
pack("src/deltachat_rpc_server/__init__.py", "")
def build_wheel(version, binary, tag, windows=False): def build_wheel(version, binary, tag, windows=False):
filename = f"deltachat_rpc_server-{version}-{tag}.whl" filename = f"deltachat_rpc_server-{version}-{tag}.whl"
@@ -168,23 +88,19 @@ def main():
with Path("Cargo.toml").open("rb") as fp: with Path("Cargo.toml").open("rb") as fp:
cargo_manifest = tomllib.load(fp) cargo_manifest = tomllib.load(fp)
version = cargo_manifest["package"]["version"] version = cargo_manifest["package"]["version"]
if sys.argv[1] == "source": arch = sys.argv[1]
filename = f"deltachat_rpc_server-{version}.tar.gz" executable = sys.argv[2]
build_source_package(version, filename) tags = arch2tags[arch]
else:
arch = sys.argv[1]
executable = sys.argv[2]
tags = arch2tags[arch]
if arch in ["win32", "win64"]: if arch in ["win32", "win64"]:
build_wheel( build_wheel(
version, version,
executable, executable,
f"py3-none-{tags}", f"py3-none-{tags}",
windows=True, windows=True,
) )
else: else:
build_wheel(version, executable, f"py3-none-{tags}") build_wheel(version, executable, f"py3-none-{tags}")
main() main()

View File

@@ -32,6 +32,7 @@ use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::{ use crate::download::{
DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD, DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD,
}; };
use crate::ensure_and_debug_assert_eq;
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers}; use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
use crate::events::EventType; use crate::events::EventType;
use crate::key::{Fingerprint, self_fingerprint}; use crate::key::{Fingerprint, self_fingerprint};
@@ -1782,9 +1783,8 @@ impl Chat {
); );
bail!("Cannot set message, contact for {} not found.", self.id); bail!("Cannot set message, contact for {} not found.", self.id);
} }
} else if matches!(self.typ, Chattype::Group | Chattype::OutBroadcast) } else if self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 ensure_and_debug_assert_eq!(self.typ, Chattype::Group,);
{
msg.param.set_int(Param::AttachChatAvatarAndDescription, 1); msg.param.set_int(Param::AttachChatAvatarAndDescription, 1);
self.param self.param
.remove(Param::Unpromoted) .remove(Param::Unpromoted)
@@ -3626,9 +3626,6 @@ pub(crate) async fn create_group_ex(
/// because the word "channel" already appears a lot in the code, /// because the word "channel" already appears a lot in the code,
/// which would make it hard to grep for it. /// which would make it hard to grep for it.
/// ///
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
/// see [`create_group`] for more information on the unpromoted state.
///
/// Returns the created chat's id. /// Returns the created chat's id.
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> { pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
let grpid = create_id(); let grpid = create_id();
@@ -3660,17 +3657,20 @@ pub(crate) async fn create_out_broadcast_ex(
|row| row.get(0), |row| row.get(0),
)?; )?;
ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}"); ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}");
let mut params: Params = Params::new();
params.update_timestamp(Param::GroupNameTimestamp, time())?;
t.execute( t.execute(
"INSERT INTO chats "INSERT INTO chats
(type, name, name_normalized, grpid, created_timestamp) (type, name, name_normalized, grpid, created_timestamp, param)
VALUES(?, ?, ?, ?, ?)", VALUES(?, ?, ?, ?, ?, ?)",
( (
Chattype::OutBroadcast, Chattype::OutBroadcast,
&chat_name, &chat_name,
normalize_text(&chat_name), normalize_text(&chat_name),
&grpid, &grpid,
timestamp, timestamp,
params.to_string(),
), ),
)?; )?;
let chat_id = ChatId::new(t.last_insert_rowid().try_into()?); let chat_id = ChatId::new(t.last_insert_rowid().try_into()?);
@@ -3738,17 +3738,19 @@ pub(crate) async fn update_chat_contacts_table(
id: ChatId, id: ChatId,
contacts: &BTreeSet<ContactId>, contacts: &BTreeSet<ContactId>,
) -> Result<()> { ) -> Result<()> {
// See add_to_chat_contacts_table() for reasoning.
let limit = cmp::max(time().saturating_add(TIMESTAMP_SENT_TOLERANCE), timestamp);
context context
.sql .sql
.transaction(move |transaction| { .transaction(move |transaction| {
// Bump `remove_timestamp` to at least `now` // Bump `remove_timestamp` even for members from `contacts`.
// even for members from `contacts`.
// We add members from `contacts` back below. // We add members from `contacts` back below.
transaction.execute( transaction.execute(
"UPDATE chats_contacts "UPDATE chats_contacts SET
SET remove_timestamp=MAX(add_timestamp+1, ?) add_timestamp=MIN(add_timestamp, ?1),
remove_timestamp=MAX(MIN(remove_timestamp,?1), MIN(add_timestamp,?1)+1, ?)
WHERE chat_id=?", WHERE chat_id=?",
(timestamp, id), (limit, timestamp, id),
)?; )?;
if !contacts.is_empty() { if !contacts.is_empty() {
@@ -3760,9 +3762,8 @@ pub(crate) async fn update_chat_contacts_table(
)?; )?;
for contact_id in contacts { for contact_id in contacts {
// We bumped `add_timestamp` for existing rows above, // We bumped `remove_timestamp` for existing rows above,
// so on conflict it is enough to set `add_timestamp = remove_timestamp` // so on conflict it is enough to set `add_timestamp = remove_timestamp`.
// and this guarantees that `add_timestamp` is no less than `timestamp`.
statement.execute((id, contact_id, timestamp))?; statement.execute((id, contact_id, timestamp))?;
} }
} }
@@ -3779,17 +3780,24 @@ pub(crate) async fn add_to_chat_contacts_table(
chat_id: ChatId, chat_id: ChatId,
contact_ids: &[ContactId], contact_ids: &[ContactId],
) -> Result<()> { ) -> Result<()> {
// Our clock may be slow, so limit stored timestamps with `timestamp` if it's bigger. This way
// we only cap remote timestamps if, in addition, remote changes arrive reordered or we do local
// changes. Also allow some tolerance, moreover, previous removals might lend time from the
// future.
let limit = cmp::max(time().saturating_add(TIMESTAMP_SENT_TOLERANCE), timestamp);
context context
.sql .sql
.transaction(move |transaction| { .transaction(move |transaction| {
let mut add_statement = transaction.prepare( let mut add_statement = transaction.prepare(
"INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp) VALUES(?1, ?2, ?3) "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp) VALUES(?1, ?2, ?3)
ON CONFLICT (chat_id, contact_id) ON CONFLICT (chat_id, contact_id)
DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)", DO UPDATE SET
remove_timestamp=MIN(remove_timestamp, ?4),
add_timestamp=MIN(MAX(add_timestamp,remove_timestamp,?3), ?4)",
)?; )?;
for contact_id in contact_ids { for contact_id in contact_ids {
add_statement.execute((chat_id, contact_id, timestamp))?; add_statement.execute((chat_id, contact_id, timestamp, limit))?;
} }
Ok(()) Ok(())
}) })
@@ -3800,26 +3808,34 @@ pub(crate) async fn add_to_chat_contacts_table(
/// Removes a contact from the chat /// Removes a contact from the chat
/// by updating the `remove_timestamp`. /// by updating the `remove_timestamp`.
/// Returns whether the contact has been a chat member recently. If so, a removal message should be
/// sent.
pub(crate) async fn remove_from_chat_contacts_table( pub(crate) async fn remove_from_chat_contacts_table(
context: &Context, context: &Context,
chat_id: ChatId, chat_id: ChatId,
contact_id: ContactId, contact_id: ContactId,
) -> Result<()> { ) -> Result<bool> {
let now = time(); let now = time();
context // See add_to_chat_contacts_table() for reasoning.
let limit = now.saturating_add(TIMESTAMP_SENT_TOLERANCE);
let is_past_member = context
.sql .sql
.execute( .execute(
"UPDATE chats_contacts "UPDATE chats_contacts SET
SET remove_timestamp=MAX(add_timestamp+1, ?) add_timestamp=MIN(add_timestamp, ?1),
remove_timestamp=MAX(MIN(remove_timestamp,?1), MIN(add_timestamp,?1)+1, ?)
WHERE chat_id=? AND contact_id=?", WHERE chat_id=? AND contact_id=?",
(now, chat_id, contact_id), (limit, now, chat_id, contact_id),
) )
.await?; .await?
Ok(()) > 0;
Ok(is_past_member)
} }
/// Removes a contact from the chat /// Removes a contact from the chat
/// without leaving a trace. /// without leaving a trace in the db.
/// Returns whether the contact was removed, even if it was a past contact. If so, a removal message
/// should be sent if the removal is issued by this device.
/// ///
/// Note that if we call this function, /// Note that if we call this function,
/// and then receive a message from another device /// and then receive a message from another device
@@ -3829,17 +3845,17 @@ pub(crate) async fn remove_from_chat_contacts_table_without_trace(
context: &Context, context: &Context,
chat_id: ChatId, chat_id: ChatId,
contact_id: ContactId, contact_id: ContactId,
) -> Result<()> { ) -> Result<bool> {
context let removed = context
.sql .sql
.execute( .execute(
"DELETE FROM chats_contacts "DELETE FROM chats_contacts
WHERE chat_id=? AND contact_id=?", WHERE chat_id=? AND contact_id=?",
(chat_id, contact_id), (chat_id, contact_id),
) )
.await?; .await?
> 0;
Ok(()) Ok(removed)
} }
/// Adds a contact to the chat. /// Adds a contact to the chat.
@@ -4159,10 +4175,13 @@ pub async fn remove_contact_from_chat(
let mut sync = Nosync; let mut sync = Nosync;
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast { let removed = if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
remove_from_chat_contacts_table(context, chat_id, contact_id).await?; remove_from_chat_contacts_table(context, chat_id, contact_id).await?
} else { } else {
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?; remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?
};
if !removed {
return Ok(());
} }
// We do not return an error if the contact does not exist in the database. // We do not return an error if the contact does not exist in the database.

View File

@@ -9,6 +9,7 @@ use crate::headerdef::HeaderDef;
use crate::imex::{ImexMode, has_backup, imex}; use crate::imex::{ImexMode, has_backup, imex};
use crate::message::{Message, MessengerMessage, delete_msgs}; use crate::message::{Message, MessengerMessage, delete_msgs};
use crate::mimeparser::{self, MimeMessage}; use crate::mimeparser::{self, MimeMessage};
use crate::qr::{Qr, check_qr};
use crate::receive_imf::receive_imf; use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin}; use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::test_utils; use crate::test_utils;
@@ -2799,6 +2800,30 @@ async fn test_can_send_group() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cant_remove_nonmember() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_broadcast_id = create_broadcast(alice, "Channel".to_string()).await?;
let qr = get_securejoin_qr(alice, Some(alice_broadcast_id))
.await
.unwrap();
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let alice_charlie_id = alice.add_or_lookup_contact_id(charlie).await;
remove_contact_from_chat(alice, alice_broadcast_id, alice_charlie_id).await?;
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
assert!(!remove_from_chat_contacts_table(alice, alice_broadcast_id, alice_charlie_id).await?);
assert!(
!remove_from_chat_contacts_table_without_trace(alice, alice_broadcast_id, alice_charlie_id)
.await?
);
Ok(())
}
/// Tests that in a broadcast channel, /// Tests that in a broadcast channel,
/// the recipients can't see the identity of their fellow recipients. /// the recipients can't see the identity of their fellow recipients.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2922,10 +2947,24 @@ async fn test_broadcast_change_name() -> Result<()> {
let fiona = &tcm.fiona().await; let fiona = &tcm.fiona().await;
let broadcast_id = create_broadcast(alice, "Channel".to_string()).await?; let broadcast_id = create_broadcast(alice, "Channel".to_string()).await?;
let qr = get_securejoin_qr(alice, Some(broadcast_id)).await.unwrap(); let mut qr = get_securejoin_qr(alice, Some(broadcast_id)).await.unwrap();
// Something goes wrong with the title, e.g. maybe it gets ellipsized
// Note that the title always comes at the end for human readability
qr += "+modified+title";
{
tcm.section("Alice invites Bob to her channel");
let Qr::AskJoinBroadcast { name, .. } = check_qr(bob, &qr).await? else {
panic!();
};
assert_eq!(name, "Channel modified title");
// The channel's name gets fixed after actually joining the channel:
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(bob_chat.name, "Channel");
}
tcm.section("Alice invites Bob to her channel");
tcm.exec_securejoin_qr(bob, alice, &qr).await;
tcm.section("Alice invites Fiona to her channel"); tcm.section("Alice invites Fiona to her channel");
tcm.exec_securejoin_qr(fiona, alice, &qr).await; tcm.exec_securejoin_qr(fiona, alice, &qr).await;
@@ -3049,6 +3088,31 @@ async fn test_broadcast_resend_to_new_member() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_broadcast_resend_failed_msg_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, "bc".to_string()).await?;
let qr = get_securejoin_qr(alice, Some(alice_bc_id)).await.unwrap();
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let alice_msg_id = alice.send_text(alice_bc_id, "text").await.sender_msg_id;
let mut msg = Message::load_from_db(alice, alice_msg_id).await?;
message::set_msg_failed(alice, &mut msg, "error").await?;
let fiona_bc_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await;
let resent_msg = alice.pop_sent_msg().await;
let fiona_msg = fiona.recv_msg(&resent_msg).await;
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
assert_eq!(fiona_msg.text, "text");
assert_eq!(
alice_msg_id.get_state(alice).await?,
MessageState::OutFailed
);
Ok(())
}
/// - Alice has multiple devices /// - Alice has multiple devices
/// - Alice creates a broadcast and sends a message into it /// - Alice creates a broadcast and sends a message into it
/// - Alice's second device sees the broadcast /// - Alice's second device sees the broadcast

View File

@@ -452,11 +452,6 @@ pub enum Config {
/// storing the same token multiple times on the server. /// storing the same token multiple times on the server.
EncryptedDeviceToken, EncryptedDeviceToken,
/// Enables running test hooks, e.g. see `InnerContext::pre_encrypt_mime_hook`.
/// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not
/// using this still run unmodified code.
TestHooks,
/// Return an error from `receive_imf_inner()`. For tests. /// Return an error from `receive_imf_inner()`. For tests.
SimulateReceiveImfError, SimulateReceiveImfError,

View File

@@ -680,6 +680,8 @@ async fn get_autoconfig(
param: &EnteredLoginParam, param: &EnteredLoginParam,
param_domain: &str, param_domain: &str,
) -> Option<Vec<ServerParams>> { ) -> Option<Vec<ServerParams>> {
let accept_invalid_certificates = param.certificate_checks.accept_invalid_certificates();
// Make sure to not encode `.` as `%2E` here. // Make sure to not encode `.` as `%2E` here.
// Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML // Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML
// when address is encoded. // when address is encoded.
@@ -696,6 +698,7 @@ async fn get_autoconfig(
"https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}" "https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
), ),
&param.addr, &param.addr,
accept_invalid_certificates,
) )
.await .await
{ {
@@ -710,6 +713,7 @@ async fn get_autoconfig(
"https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}" "https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
), ),
&param.addr, &param.addr,
accept_invalid_certificates,
) )
.await .await
{ {
@@ -721,6 +725,7 @@ async fn get_autoconfig(
if let Ok(res) = outlk_autodiscover( if let Ok(res) = outlk_autodiscover(
ctx, ctx,
format!("https://{param_domain}/autodiscover/autodiscover.xml"), format!("https://{param_domain}/autodiscover/autodiscover.xml"),
accept_invalid_certificates,
) )
.await .await
{ {
@@ -731,6 +736,7 @@ async fn get_autoconfig(
if let Ok(res) = outlk_autodiscover( if let Ok(res) = outlk_autodiscover(
ctx, ctx,
format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",), format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",),
accept_invalid_certificates,
) )
.await .await
{ {
@@ -743,6 +749,7 @@ async fn get_autoconfig(
ctx, ctx,
&format!("https://autoconfig.thunderbird.net/v1.1/{param_domain}"), &format!("https://autoconfig.thunderbird.net/v1.1/{param_domain}"),
&param.addr, &param.addr,
accept_invalid_certificates,
) )
.await .await
{ {

View File

@@ -10,7 +10,7 @@ use quick_xml::events::{BytesStart, Event};
use super::{Error, ServerParams}; use super::{Error, ServerParams};
use crate::context::Context; use crate::context::Context;
use crate::log::warn; use crate::log::warn;
use crate::net::read_url; use crate::net::read_url_with_tls;
use crate::provider::{Protocol, Socket}; use crate::provider::{Protocol, Socket};
#[derive(Debug)] #[derive(Debug)]
@@ -249,8 +249,9 @@ pub(crate) async fn moz_autoconfigure(
context: &Context, context: &Context,
url: &str, url: &str,
addr: &str, addr: &str,
accept_invalid_certificates: bool,
) -> Result<Vec<ServerParams>, Error> { ) -> Result<Vec<ServerParams>, Error> {
let xml_raw = read_url(context, url).await?; let xml_raw = read_url_with_tls(context, url, !accept_invalid_certificates).await?;
let res = parse_serverparams(addr, &xml_raw); let res = parse_serverparams(addr, &xml_raw);
if let Err(err) = &res { if let Err(err) = &res {

View File

@@ -10,7 +10,7 @@ use quick_xml::events::Event;
use super::{Error, ServerParams}; use super::{Error, ServerParams};
use crate::context::Context; use crate::context::Context;
use crate::log::warn; use crate::log::warn;
use crate::net::read_url; use crate::net::read_url_with_tls;
use crate::provider::{Protocol, Socket}; use crate::provider::{Protocol, Socket};
/// Result of parsing a single `Protocol` tag. /// Result of parsing a single `Protocol` tag.
@@ -196,10 +196,11 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
pub(crate) async fn outlk_autodiscover( pub(crate) async fn outlk_autodiscover(
context: &Context, context: &Context,
mut url: String, mut url: String,
accept_invalid_certificates: bool,
) -> Result<Vec<ServerParams>, Error> { ) -> Result<Vec<ServerParams>, Error> {
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */ /* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
for _i in 0..10 { for _i in 0..10 {
let xml_raw = read_url(context, &url).await?; let xml_raw = read_url_with_tls(context, &url, !accept_invalid_certificates).await?;
let res = parse_xml(&xml_raw); let res = parse_xml(&xml_raw);
if let Err(err) = &res { if let Err(err) = &res {
warn!(context, "{}", err); warn!(context, "{}", err);

View File

@@ -332,17 +332,6 @@ pub struct InnerContext {
/// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity, /// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
/// see [`Context::get_connectivity()`]. /// see [`Context::get_connectivity()`].
pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>, pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
#[expect(clippy::type_complexity)]
/// Transforms the root of the cryptographic payload before encryption.
pub(crate) pre_encrypt_mime_hook: parking_lot::Mutex<
Option<
for<'a> fn(
&Context,
mail_builder::mime::MimePart<'a>,
) -> mail_builder::mime::MimePart<'a>,
>,
>,
} }
/// The state of ongoing process. /// The state of ongoing process.
@@ -522,7 +511,6 @@ impl Context {
self_fingerprint: OnceLock::new(), self_fingerprint: OnceLock::new(),
self_public_key: Mutex::new(None), self_public_key: Mutex::new(None),
connectivities: parking_lot::Mutex::new(Vec::new()), connectivities: parking_lot::Mutex::new(Vec::new()),
pre_encrypt_mime_hook: None.into(),
}; };
let ctx = Context { let ctx = Context {

View File

@@ -6,6 +6,7 @@ use anyhow::{Result, anyhow, bail, ensure};
use deltachat_derive::{FromSql, ToSql}; use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context; use crate::context::Context;
use crate::imap::session::Session; use crate::imap::session::Session;
use crate::log::warn; use crate::log::warn;
@@ -169,7 +170,8 @@ pub(crate) async fn download_msg(
} }
Box::pin(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?;
if ephemeral::should_delete_all_downloaded_messages(context, session.is_chatmail()).await? { let bcc_self = context.get_config_bool(Config::BccSelf).await?;
if ephemeral::should_delete_all_downloaded_messages(bcc_self, session.is_chatmail()) {
// Now that the message was downloaded, it likely needs to be deleted; // Now that the message was downloaded, it likely needs to be deleted;
// trigger a re-check by interrupting the inbox folder. // trigger a re-check by interrupting the inbox folder.
// This is mainly needed to make the tests pass; // This is mainly needed to make the tests pass;

View File

@@ -654,7 +654,7 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
/// Schedules expired IMAP messages for deletion on the server. /// Schedules expired IMAP messages for deletion on the server.
/// ///
/// Also see [`delete_expired_imap_messages`], /// Also see [`delete_expired_messages`],
/// which locally deletes expired messages. /// which locally deletes expired messages.
pub(crate) async fn delete_expired_imap_messages( pub(crate) async fn delete_expired_imap_messages(
context: &Context, context: &Context,
@@ -663,7 +663,8 @@ pub(crate) async fn delete_expired_imap_messages(
) -> Result<()> { ) -> Result<()> {
let now = time(); let now = time();
if should_delete_all_downloaded_messages(context, is_chatmail).await? { let bcc_self = context.get_config_bool(Config::BccSelf).await?;
if should_delete_all_downloaded_messages(bcc_self, is_chatmail) {
// This is the only device using this relay. // This is the only device using this relay.
// Mark all downloaded messages for deletion, because they are not needed anymore. // Mark all downloaded messages for deletion, because they are not needed anymore.
// //
@@ -690,7 +691,7 @@ pub(crate) async fn delete_expired_imap_messages(
(transport_id, now, DownloadState::Done), (transport_id, now, DownloadState::Done),
) )
.await?; .await?;
} else { } else if bcc_self {
// There may be other devices using this relay, // There may be other devices using this relay,
// either because there is multi-device or because this is a classical email server. // either because there is multi-device or because this is a classical email server.
// Only delete expired ephemeral messages. // Only delete expired ephemeral messages.
@@ -711,16 +712,37 @@ pub(crate) async fn delete_expired_imap_messages(
(transport_id, now), (transport_id, now),
) )
.await?; .await?;
} else {
// Single device.
// Delete all expired and encrypted messages.
context
.sql
.execute(
"UPDATE imap
SET target=''
WHERE transport_id=?1
AND rfc724_mid IN (
SELECT rfc724_mid FROM msgs
WHERE id>9
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
((param GLOB '*\nc=1*' OR param GLOB 'c=1*') AND download_state=?3))
UNION
SELECT pre_rfc724_mid FROM msgs
WHERE pre_rfc724_mid!=''
AND id>9
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
(param GLOB '*\nc=1*' OR param GLOB 'c=1*'))
)",
(transport_id, now, DownloadState::Done),
)
.await?;
} }
Ok(()) Ok(())
} }
pub(crate) async fn should_delete_all_downloaded_messages( pub(crate) fn should_delete_all_downloaded_messages(bcc_self: bool, is_chatmail: bool) -> bool {
context: &Context, !bcc_self && is_chatmail
is_chatmail: bool,
) -> Result<bool> {
Ok(!context.get_config_bool(Config::BccSelf).await? && is_chatmail)
} }
/// Start ephemeral timers for seen messages if they are not started /// Start ephemeral timers for seen messages if they are not started

View File

@@ -478,9 +478,10 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
// Test messages: // Test messages:
// //
// Three messages that were not split into pre- and post- message: // Four messages that were not split into pre- and post- message:
// "expired@localhost" - expired ephemeral message // "expired@localhost" - expired ephemeral message
// "no_expire@localhost" - non-ephemeral message // "no_expire@localhost" - non-ephemeral message
// "no_expire_unencrypted@localhost" - non-ephemeral message, not encrypted
// "future@localhost" - will expire in the future, but not yet // "future@localhost" - will expire in the future, but not yet
// //
// And four messages that were split into pre- and post-message. // And four messages that were split into pre- and post-message.
@@ -489,57 +490,81 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
// "future_*@localhost" - has pre-msg, not expired yet, not downloaded yet // "future_*@localhost" - has pre-msg, not expired yet, not downloaded yet
// "done_*@localhost" - Fully downloaded -> post- message can be deleted // "done_*@localhost" - Fully downloaded -> post- message can be deleted
// //
// The tuple is (rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid) // The tuple is (rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid, is_encrypted)
let msgs: [(&str, i64, DownloadState, &str); 7] = [ let msgs: [(&str, i64, DownloadState, &str, bool); 8] = [
("expired@localhost", now - 1, DownloadState::Done, ""), ("expired@localhost", now - 1, DownloadState::Done, "", true),
("no_expire@localhost", 0, DownloadState::Done, ""), ("no_expire@localhost", 0, DownloadState::Done, "", true),
(
"no_expire_unencrypted@localhost",
0,
DownloadState::Done,
"",
false,
),
// Use "now + 3600" rather than "now + 1", otherwise the test may be flaky // Use "now + 3600" rather than "now + 1", otherwise the test may be flaky
// if it is slow and the message expires in a second // if it is slow and the message expires in a second
("future@localhost", now + 3600, DownloadState::Done, ""), (
"future@localhost",
now + 3600,
DownloadState::Done,
"",
true,
),
( (
"expired_post@localhost", "expired_post@localhost",
now - 1, now - 1,
DownloadState::Available, DownloadState::Available,
"expired_pre@localhost", "expired_pre@localhost",
true,
), ),
( (
"no_expire_post@localhost", "no_expire_post@localhost",
0, 0,
DownloadState::Available, DownloadState::Available,
"no_expire_pre@localhost", "no_expire_pre@localhost",
true,
), ),
( (
"future_post@localhost", "future_post@localhost",
now + 3600, now + 3600,
DownloadState::Available, DownloadState::Available,
"future_pre@localhost", "future_pre@localhost",
true,
), ),
( (
"done_post@localhost", "done_post@localhost",
0, 0,
DownloadState::Done, DownloadState::Done,
"done_pre@localhost", "done_pre@localhost",
true,
), ),
]; ];
for (rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid) in msgs { for (rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid, is_encrypted) in msgs {
t.sql t.sql
.execute( .execute(
"INSERT INTO msgs \ "INSERT INTO msgs \
(rfc724_mid, timestamp, ephemeral_timestamp, download_state, pre_rfc724_mid) \ (rfc724_mid, timestamp, ephemeral_timestamp, download_state, pre_rfc724_mid, param) \
VALUES (?,?,?,?,?)", VALUES (?,?,?,?,?,?)",
( (
rfc724_mid, rfc724_mid,
now, now,
ephemeral_timestamp, ephemeral_timestamp,
download_state, download_state,
pre_rfc724_mid, pre_rfc724_mid,
if is_encrypted {
"c=1"
} else {
""
}
), ),
) )
.await?; .await?;
} }
let rfc724_mids: Vec<&str> = msgs let rfc724_mids: Vec<&str> = msgs
.iter() .iter()
.flat_map(|(rfc724_mid, _, _, pre_rfc724_mid)| [*rfc724_mid, *pre_rfc724_mid]) .flat_map(|(rfc724_mid, _, _, pre_rfc724_mid, _is_encrypted)| {
[*rfc724_mid, *pre_rfc724_mid]
})
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(); .collect();
@@ -554,13 +579,22 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
.await?; .await?;
} }
for (is_chatmail, other_transport) in for (is_chatmail, other_transport, bcc_self) in [
[(false, false), (false, true), (true, false), (true, true)] (false, false, false),
{ (false, false, true),
(false, true, false),
(false, true, true),
(true, false, false),
(true, false, true),
(true, true, false),
(true, true, true),
] {
println!( println!(
"Testing combination is_chatmail={is_chatmail}, other_transport={other_transport}" "Testing combination is_chatmail={is_chatmail}, other_transport={other_transport}, bcc_self={bcc_self}"
); );
t.set_config_bool(Config::BccSelf, bcc_self).await?;
delete_expired_imap_messages( delete_expired_imap_messages(
&t, &t,
if other_transport { if other_transport {
@@ -581,19 +615,20 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
} }
assert_eq!(is_deleted(&t, "expired@localhost").await?, true); assert_eq!(is_deleted(&t, "expired@localhost").await?, true);
assert_eq!(is_deleted(&t, "no_expire@localhost").await?, is_chatmail); assert_eq!(is_deleted(&t, "no_expire@localhost").await?, !bcc_self);
assert_eq!(is_deleted(&t, "future@localhost").await?, is_chatmail); assert_eq!(
is_deleted(&t, "no_expire_unencrypted@localhost").await?,
is_chatmail && !bcc_self
);
assert_eq!(is_deleted(&t, "future@localhost").await?, !bcc_self);
assert_eq!(is_deleted(&t, "expired_post@localhost").await?, true); assert_eq!(is_deleted(&t, "expired_post@localhost").await?, true);
assert_eq!(is_deleted(&t, "expired_pre@localhost").await?, true); assert_eq!(is_deleted(&t, "expired_pre@localhost").await?, true);
assert_eq!(is_deleted(&t, "no_expire_post@localhost").await?, false); assert_eq!(is_deleted(&t, "no_expire_post@localhost").await?, false);
assert_eq!( assert_eq!(is_deleted(&t, "no_expire_pre@localhost").await?, !bcc_self);
is_deleted(&t, "no_expire_pre@localhost").await?,
is_chatmail
);
assert_eq!(is_deleted(&t, "future_post@localhost").await?, false); assert_eq!(is_deleted(&t, "future_post@localhost").await?, false);
assert_eq!(is_deleted(&t, "future_pre@localhost").await?, is_chatmail); assert_eq!(is_deleted(&t, "future_pre@localhost").await?, !bcc_self);
assert_eq!(is_deleted(&t, "done_pre@localhost").await?, is_chatmail); assert_eq!(is_deleted(&t, "done_pre@localhost").await?, !bcc_self);
assert_eq!(is_deleted(&t, "done_post@localhost").await?, is_chatmail); assert_eq!(is_deleted(&t, "done_post@localhost").await?, !bcc_self);
reset_targets(&t).await; reset_targets(&t).await;
} }

View File

@@ -56,6 +56,15 @@ pub enum EnteredCertificateChecks {
AcceptInvalidCertificates2 = 3, AcceptInvalidCertificates2 = 3,
} }
impl EnteredCertificateChecks {
pub(crate) fn accept_invalid_certificates(self) -> bool {
matches!(
self,
Self::AcceptInvalidCertificates | Self::AcceptInvalidCertificates2
)
}
}
/// Login parameters for a single IMAP server. /// Login parameters for a single IMAP server.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnteredImapLoginParam { pub struct EnteredImapLoginParam {

View File

@@ -140,8 +140,20 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
Ok(()) Ok(())
} }
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> { /// Returns whether the message state is updated to `OutDelivered`.
update_msg_state(context, self, MessageState::OutDelivered).await?; pub(crate) async fn set_delivered(self, context: &Context) -> Result<bool> {
if context
.sql
.execute(
// Only update `OutPending` i.e. if the message is (re-)sent to all chat members.
"UPDATE msgs SET state=?, error='' WHERE id=? AND state=?",
(MessageState::OutDelivered, self, MessageState::OutPending),
)
.await?
== 0
{
return Ok(false);
}
let chat_id: Option<ChatId> = context let chat_id: Option<ChatId> = context
.sql .sql
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,)) .query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,))
@@ -153,7 +165,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
if let Some(chat_id) = chat_id { if let Some(chat_id) = chat_id {
chatlist_events::emit_chatlist_item_changed(context, chat_id); chatlist_events::emit_chatlist_item_changed(context, chat_id);
} }
Ok(()) Ok(true)
} }
/// Bad evil escape hatch. /// Bad evil escape hatch.
@@ -312,6 +324,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
if duration != 0 { if duration != 0 {
ret += &format!("Duration: {duration} ms\n",); ret += &format!("Duration: {duration} ms\n",);
} }
ret += &format!("\nDatabase ID: {}", msg.id);
if !msg.rfc724_mid.is_empty() { if !msg.rfc724_mid.is_empty() {
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid); ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
@@ -602,6 +615,31 @@ impl Message {
Ok(msg) Ok(msg)
} }
/// Loads the message with given Message-ID from the database.
///
/// Cannot return a trashed message.
pub async fn load_by_rfc724_mid_optional(
context: &Context,
rfc724_mid: &str,
) -> Result<Option<Message>> {
if let Some(msg_id) = context
.sql
.query_row_optional(
"SELECT id FROM msgs WHERE rfc724_mid=? AND chat_id != ?",
(rfc724_mid, DC_CHAT_ID_TRASH),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
)
.await?
{
Self::load_from_db_optional(context, msg_id).await
} else {
Ok(None)
}
}
/// Returns additional text which is appended to the message's text field /// Returns additional text which is appended to the message's text field
/// when it is loaded from the database. /// when it is loaded from the database.
/// Currently this is used to add infomation to pre-messages of what the download will be and how large it is /// Currently this is used to add infomation to pre-messages of what the download will be and how large it is
@@ -1389,6 +1427,9 @@ pub enum MessageState {
/// The user has pressed the "send" button but the message is not /// 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 /// yet sent and is pending in some way. Maybe we're offline (no
/// checkmark). /// checkmark).
///
/// This state means that the message is being (re-)sent to all chat members. It shalln't be
/// used e.g. for resending only to a new broadcast member.
OutPending = 20, OutPending = 20,
/// *Unrecoverable* error (*recoverable* errors result in pending /// *Unrecoverable* error (*recoverable* errors result in pending
@@ -2001,13 +2042,6 @@ pub(crate) async fn update_msg_state(
Ok(()) Ok(())
} }
// as we do not cut inside words, this results in about 32-42 characters.
// Do not use too long subjects - we add a tag after the subject which gets truncated by the clients otherwise.
// It should also be very clear, the subject is _not_ the whole message.
// The value is also used for CC:-summaries
// Context functions to work with messages
pub(crate) async fn set_msg_failed( pub(crate) async fn set_msg_failed(
context: &Context, context: &Context,
msg: &mut Message, msg: &mut Message,

View File

@@ -1168,12 +1168,6 @@ impl MimeFactory {
_ => None, _ => None,
}; };
if context.get_config_bool(Config::TestHooks).await?
&& let Some(hook) = &*context.pre_encrypt_mime_hook.lock()
{
message = hook(context, message);
}
let encrypted = if let Some(shared_secret) = shared_secret { let encrypted = if let Some(shared_secret) = shared_secret {
let sign = true; let sign = true;
encrypt_helper encrypt_helper

View File

@@ -1790,35 +1790,27 @@ async fn test_time_in_future() -> Result<()> {
Ok(()) Ok(())
} }
/// Tests receiving a message with RFC 9788 header protection and legacy display element.
///
/// Legacy display elements should not be rendered:
/// <https://www.rfc-editor.org/rfc/rfc9788.html#name-do-not-render-legacy-displa>
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hp_legacy_display() -> Result<()> { async fn test_hp_legacy_display() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await; let bob = &tcm.bob().await;
let mut msg = Message::new_text( let msg_id = receive_imf(
"Subject: Dinner plans\n\ bob,
\n\ include_bytes!("../../test-data/message/hp_legacy_display.eml"),
Let's eat" false,
.to_string(), )
); .await?
msg.set_subject("Dinner plans".to_string()); .unwrap()
let chat_id = alice.create_chat(bob).await.id; .msg_ids[0];
alice.set_config_bool(Config::TestHooks, true).await?; let msg_bob = Message::load_from_db(bob, msg_id).await?;
*alice.pre_encrypt_mime_hook.lock() = Some(|_, mut mime| {
for (h, v) in &mut mime.headers {
if h == "Content-Type"
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
{
*ct = ct.clone().attribute("hp-legacy-display", "1");
}
}
mime
});
let sent_msg = alice.send_msg(chat_id, &mut msg).await;
let msg_bob = bob.recv_msg(&sent_msg).await;
assert_eq!(msg_bob.subject, "Dinner plans"); assert_eq!(msg_bob.subject, "Dinner plans");
// Legacy display element is removed from the text/plain body.
assert_eq!(msg_bob.text, "Let's eat"); assert_eq!(msg_bob.text, "Let's eat");
Ok(()) Ok(())
} }

View File

@@ -23,6 +23,7 @@ pub(crate) mod session;
pub(crate) mod tls; pub(crate) mod tls;
use dns::lookup_host_with_cache; use dns::lookup_host_with_cache;
pub(crate) use http::read_url_with_tls;
pub use http::{Response as HttpResponse, read_url, read_url_blob}; pub use http::{Response as HttpResponse, read_url, read_url_blob};
use tls::wrap_tls; use tls::wrap_tls;

View File

@@ -231,7 +231,10 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
HashMap::from([ HashMap::from([
( (
"imap.163.com", "imap.163.com",
vec![IpAddr::V4(Ipv4Addr::new(111, 124, 203, 45))], vec![
IpAddr::V4(Ipv4Addr::new(111, 124, 203, 45)),
IpAddr::V4(Ipv4Addr::new(111, 124, 203, 50)),
],
), ),
( (
"smtp.163.com", "smtp.163.com",
@@ -422,12 +425,12 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"nine.testrun.org", "nine.testrun.org",
vec![ vec![
IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)), IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)),
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
IpAddr::V4(Ipv4Addr::new(216, 144, 228, 100)), IpAddr::V4(Ipv4Addr::new(216, 144, 228, 100)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)), IpAddr::V4(Ipv4Addr::new(77, 42, 49, 41)),
IpAddr::V6(Ipv6Addr::new( IpAddr::V6(Ipv6Addr::new(
0x2001, 0x41d0, 0x701, 0x1100, 0, 0, 0, 0x8ab1, 0x2001, 0x41d0, 0x701, 0x1100, 0, 0, 0, 0x8ab1,
)), )),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f9, 0xfff1, 0x59, 0, 0, 0, 1)),
], ],
), ),
( (
@@ -697,6 +700,10 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"chatmail.hackea.org", "chatmail.hackea.org",
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 11, 85))], vec![IpAddr::V4(Ipv4Addr::new(82, 165, 11, 85))],
), ),
(
"chat.adminforge.de",
vec![IpAddr::V4(Ipv4Addr::new(94, 130, 17, 142))],
),
( (
"chika.aangat.lahat.computer", "chika.aangat.lahat.computer",
vec![IpAddr::V4(Ipv4Addr::new(71, 19, 150, 113))], vec![IpAddr::V4(Ipv4Addr::new(71, 19, 150, 113))],
@@ -738,6 +745,46 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"danneskjold.de", "danneskjold.de",
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 216, 132))], vec![IpAddr::V4(Ipv4Addr::new(46, 62, 216, 132))],
), ),
(
"chat.in-the.eu",
vec![IpAddr::V4(Ipv4Addr::new(78, 46, 190, 129))],
),
(
"chat.nuvon.app",
vec![IpAddr::V4(Ipv4Addr::new(178, 238, 38, 165))],
),
(
"nibblehole.com",
vec![IpAddr::V4(Ipv4Addr::new(94, 247, 42, 209))],
),
(
"chat.zashm.org",
vec![IpAddr::V4(Ipv4Addr::new(91, 245, 76, 39))],
),
(
"chat.sus.fr",
vec![IpAddr::V4(Ipv4Addr::new(152, 67, 76, 190))],
),
(
"delta.thelab.uno",
vec![IpAddr::V4(Ipv4Addr::new(146, 59, 228, 39))],
),
(
"chat.vim.wtf",
vec![IpAddr::V4(Ipv4Addr::new(116, 203, 206, 170))],
),
(
"uninterest.ing",
vec![IpAddr::V4(Ipv4Addr::new(172, 245, 70, 237))],
),
(
"sweetfern.net",
vec![IpAddr::V4(Ipv4Addr::new(178, 156, 228, 133))],
),
(
"delta.disobey.net",
vec![IpAddr::V4(Ipv4Addr::new(37, 74, 102, 44))],
),
( (
"darkrun.dev", "darkrun.dev",
vec![IpAddr::V4(Ipv4Addr::new(72, 11, 149, 146))], vec![IpAddr::V4(Ipv4Addr::new(72, 11, 149, 146))],

View File

@@ -13,7 +13,7 @@ use crate::context::Context;
use crate::log::warn; use crate::log::warn;
use crate::net::proxy::ProxyConfig; use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream; use crate::net::session::SessionStream;
use crate::net::tls::wrap_rustls; use crate::net::tls::wrap_tls;
use crate::tools::time; use crate::tools::time;
/// User-Agent for HTTP requests if a resource usage policy requires it. /// User-Agent for HTTP requests if a resource usage policy requires it.
@@ -35,7 +35,16 @@ pub struct Response {
/// Retrieves the text contents of URL using HTTP GET request. /// Retrieves the text contents of URL using HTTP GET request.
pub async fn read_url(context: &Context, url: &str) -> Result<String> { pub async fn read_url(context: &Context, url: &str) -> Result<String> {
let response = read_url_blob(context, url).await?; read_url_with_tls(context, url, true).await
}
/// Retrieves the text contents of URL using HTTP GET request.
pub(crate) async fn read_url_with_tls(
context: &Context,
url: &str,
strict_tls: bool,
) -> Result<String> {
let response = read_url_blob_with_tls(context, url, strict_tls).await?;
let text = String::from_utf8_lossy(&response.blob); let text = String::from_utf8_lossy(&response.blob);
Ok(text.to_string()) Ok(text.to_string())
} }
@@ -43,6 +52,7 @@ pub async fn read_url(context: &Context, url: &str) -> Result<String> {
async fn get_http_sender<B>( async fn get_http_sender<B>(
context: &Context, context: &Context,
parsed_url: hyper::Uri, parsed_url: hyper::Uri,
strict_tls: bool,
) -> Result<hyper::client::conn::http1::SendRequest<B>> ) -> Result<hyper::client::conn::http1::SendRequest<B>>
where where
B: hyper::body::Body + 'static + Send, B: hyper::body::Body + 'static + Send,
@@ -76,37 +86,29 @@ where
let port = parsed_url.port_u16().unwrap_or(443); let port = parsed_url.port_u16().unwrap_or(443);
let (use_sni, load_cache) = (true, true); let (use_sni, load_cache) = (true, true);
if let Some(proxy_config) = proxy_config_opt { let tcp_stream: Box<dyn SessionStream> = if let Some(proxy_config) = proxy_config_opt {
let proxy_stream = proxy_config let proxy_stream = proxy_config
.connect(context, host, port, load_cache) .connect(context, host, port, load_cache)
.await?; .await?;
let tls_stream = wrap_rustls( Box::new(proxy_stream)
host,
port,
use_sni,
"",
proxy_stream,
&context.tls_session_store,
&context.spki_hash_store,
&context.sql,
)
.await?;
Box::new(tls_stream)
} else { } else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
let tls_stream = wrap_rustls( Box::new(tcp_stream)
host, };
port,
use_sni, let tls_stream = wrap_tls(
"", strict_tls,
tcp_stream, host,
&context.tls_session_store, port,
&context.spki_hash_store, use_sni,
&context.sql, "",
) tcp_stream,
.await?; &context.tls_session_store,
Box::new(tls_stream) &context.spki_hash_store,
} &context.sql,
)
.await?;
Box::new(tls_stream)
} }
_ => bail!("Unknown URL scheme"), _ => bail!("Unknown URL scheme"),
}; };
@@ -260,7 +262,7 @@ pub(crate) async fn http_cache_cleanup(context: &Context) -> Result<()> {
/// Fetches URL and updates the cache. /// Fetches URL and updates the cache.
/// ///
/// URL is fetched regardless of whether there is an existing result in the cache. /// URL is fetched regardless of whether there is an existing result in the cache.
async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> { async fn fetch_url(context: &Context, original_url: &str, strict_tls: bool) -> Result<Response> {
let mut url = original_url.to_string(); let mut url = original_url.to_string();
// Follow up to 10 http-redirects // Follow up to 10 http-redirects
@@ -269,7 +271,7 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
.parse::<hyper::Uri>() .parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?; .with_context(|| format!("Failed to parse URL {url:?}"))?;
let mut sender = get_http_sender(context, parsed_url.clone()).await?; let mut sender = get_http_sender(context, parsed_url.clone(), strict_tls).await?;
let authority = parsed_url let authority = parsed_url
.authority() .authority()
.context("URL has no authority")? .context("URL has no authority")?
@@ -339,8 +341,10 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
mimetype, mimetype,
encoding, encoding,
}; };
info!(context, "Inserting {original_url:?} into cache."); if strict_tls {
http_cache_put(context, &url, &response).await?; info!(context, "Inserting {original_url:?} into cache.");
http_cache_put(context, &url, &response).await?;
}
return Ok(response); return Ok(response);
} }
@@ -349,6 +353,23 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
/// Retrieves the binary contents of URL using HTTP GET request. /// Retrieves the binary contents of URL using HTTP GET request.
pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> { pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
read_url_blob_with_tls(context, url, true).await
}
/// Retrieves the binary contents of URL using HTTP GET request.
pub(crate) async fn read_url_blob_with_tls(
context: &Context,
url: &str,
strict_tls: bool,
) -> Result<Response> {
if !strict_tls {
info!(
context,
"Fetching {url:?} without HTTP cache due to relaxed TLS."
);
return fetch_url(context, url, strict_tls).await;
}
if let Some((response, is_stale)) = http_cache_get(context, url).await? { if let Some((response, is_stale)) = http_cache_get(context, url).await? {
info!(context, "Returning {url:?} from cache."); info!(context, "Returning {url:?} from cache.");
if is_stale { if is_stale {
@@ -357,7 +378,7 @@ pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
tokio::spawn(async move { tokio::spawn(async move {
// Fetch URL in background to update the cache. // Fetch URL in background to update the cache.
info!(context, "Fetching stale {url:?} in background."); info!(context, "Fetching stale {url:?} in background.");
if let Err(err) = fetch_url(&context, &url).await { if let Err(err) = fetch_url(&context, &url, true).await {
warn!(context, "Failed to revalidate {url:?}: {err:#}."); warn!(context, "Failed to revalidate {url:?}: {err:#}.");
} }
}); });
@@ -366,7 +387,7 @@ pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
} }
info!(context, "Not found {url:?} in cache, fetching."); info!(context, "Not found {url:?} in cache, fetching.");
let response = fetch_url(context, url).await?; let response = fetch_url(context, url, true).await?;
Ok(response) Ok(response)
} }
@@ -384,7 +405,7 @@ pub(crate) async fn post_empty(context: &Context, url: &str) -> Result<(String,
bail!("POST requests to non-HTTPS URLs are not allowed"); bail!("POST requests to non-HTTPS URLs are not allowed");
} }
let mut sender = get_http_sender(context, parsed_url.clone()).await?; let mut sender = get_http_sender(context, parsed_url.clone(), true).await?;
let authority = parsed_url let authority = parsed_url
.authority() .authority()
.context("URL has no authority")? .context("URL has no authority")?
@@ -418,7 +439,7 @@ pub(crate) async fn post_string(context: &Context, url: &str, body: String) -> R
bail!("POST requests to non-HTTPS URLs are not allowed"); bail!("POST requests to non-HTTPS URLs are not allowed");
} }
let mut sender = get_http_sender(context, parsed_url.clone()).await?; let mut sender = get_http_sender(context, parsed_url.clone(), true).await?;
let authority = parsed_url let authority = parsed_url
.authority() .authority()
.context("URL has no authority")? .context("URL has no authority")?
@@ -449,7 +470,7 @@ pub(crate) async fn post_form<T: Serialize + ?Sized>(
} }
let encoded_body = serde_urlencoded::to_string(form).context("Failed to encode data")?; let encoded_body = serde_urlencoded::to_string(form).context("Failed to encode data")?;
let mut sender = get_http_sender(context, parsed_url.clone()).await?; let mut sender = get_http_sender(context, parsed_url.clone(), true).await?;
let authority = parsed_url let authority = parsed_url
.authority() .authority()
.context("URL has no authority")? .context("URL has no authority")?

View File

@@ -50,7 +50,7 @@ impl ShadowsocksConfig {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpConfig { pub struct HttpConfig {
/// HTTP proxy host. /// HTTP proxy host.
pub host: String, pub host: url::Host,
/// HTTP proxy port. /// HTTP proxy port.
pub port: u16, pub port: u16,
@@ -63,10 +63,7 @@ pub struct HttpConfig {
impl HttpConfig { impl HttpConfig {
fn from_url(url: Url) -> Result<Self> { fn from_url(url: Url) -> Result<Self> {
let host = url let host = url.host().context("HTTP proxy URL has no host")?.to_owned();
.host_str()
.context("HTTP proxy URL has no host")?
.to_string();
let port = url let port = url
.port_or_known_default() .port_or_known_default()
.context("HTTP(S) URLs are guaranteed to return Some port")?; .context("HTTP(S) URLs are guaranteed to return Some port")?;
@@ -104,7 +101,8 @@ impl HttpConfig {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Socks5Config { pub struct Socks5Config {
pub host: String, /// Hostname or IP address.
pub host: url::Host,
pub port: u16, pub port: u16,
pub user_password: Option<(String, String)>, pub user_password: Option<(String, String)>,
} }
@@ -117,7 +115,13 @@ impl Socks5Config {
target_port: u16, target_port: u16,
load_dns_cache: bool, load_dns_cache: bool,
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> { ) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache) let hostname = match &self.host {
url::Host::Domain(domain) => domain.to_string(),
url::Host::Ipv4(addr) => addr.to_string(),
url::Host::Ipv6(addr) => addr.to_string(),
};
let tcp_stream = connect_tcp(context, &hostname, self.port, load_dns_cache)
.await .await
.context("Failed to connect to SOCKS5 proxy")?; .context("Failed to connect to SOCKS5 proxy")?;
@@ -273,10 +277,7 @@ impl ProxyConfig {
// Because of this we do not distinguish // Because of this we do not distinguish
// between `socks5` and `socks5h`. // between `socks5` and `socks5h`.
"socks5" => { "socks5" => {
let host = url let host = url.host().context("socks5 URL has no host")?.to_owned();
.host_str()
.context("socks5 URL has no host")?
.to_string();
let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT); let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT);
let user_password = if let Some(password) = url.password() { let user_password = if let Some(password) = url.password() {
let username = percent_encoding::percent_decode_str(url.username()) let username = percent_encoding::percent_decode_str(url.username())
@@ -402,13 +403,14 @@ impl ProxyConfig {
match self { match self {
ProxyConfig::Http(http_config) => { ProxyConfig::Http(http_config) => {
let load_cache = false; let load_cache = false;
let tcp_stream = crate::net::connect_tcp( let hostname = match &http_config.host {
context, url::Host::Domain(domain) => domain.to_string(),
&http_config.host, url::Host::Ipv4(addr) => addr.to_string(),
http_config.port, url::Host::Ipv6(addr) => addr.to_string(),
load_cache, };
) let tcp_stream =
.await?; crate::net::connect_tcp(context, &hostname, http_config.port, load_cache)
.await?;
let auth = if let Some((username, password)) = &http_config.user_password { let auth = if let Some((username, password)) = &http_config.user_password {
Some((username.as_str(), password.as_str())) Some((username.as_str(), password.as_str()))
} else { } else {
@@ -419,16 +421,18 @@ impl ProxyConfig {
} }
ProxyConfig::Https(https_config) => { ProxyConfig::Https(https_config) => {
let load_cache = true; let load_cache = true;
let tcp_stream = crate::net::connect_tcp( let hostname = match &https_config.host {
context, url::Host::Domain(domain) => domain.to_string(),
&https_config.host, url::Host::Ipv4(addr) => addr.to_string(),
https_config.port, url::Host::Ipv6(addr) => addr.to_string(),
load_cache, };
)
.await?; let tcp_stream =
crate::net::connect_tcp(context, &hostname, https_config.port, load_cache)
.await?;
let use_sni = true; let use_sni = true;
let tls_stream = wrap_rustls( let tls_stream = wrap_rustls(
&https_config.host, &hostname,
https_config.port, https_config.port,
use_sni, use_sni,
"", "",
@@ -500,6 +504,7 @@ mod tests {
use super::*; use super::*;
use crate::config::Config; use crate::config::Config;
use crate::test_utils::TestContext; use crate::test_utils::TestContext;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test] #[test]
fn test_socks5_url() { fn test_socks5_url() {
@@ -507,17 +512,35 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), // IPv4 address is parsed as a domain and not url::Host::Ipv4.
// This is expected: <https://github.com/servo/rust-url/issues/767>.
// We only need a distinction for IPv6 to remove square brackets
// before passing the address to `lookup_host()`.
host: url::Host::Domain("127.0.0.1".to_string()),
port: 9050, port: 9050,
user_password: None user_password: None
}) })
); );
assert_eq!(proxy_config.to_url(), "socks5://127.0.0.1:9050".to_string());
let proxy_config = ProxyConfig::from_url("socks5://[::1]:9050").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
// IPv6 address should be recognized as IPv6 address and not "[::1]" hostname.
// Otherwise we may try to resolve "[::1]" and fail to connect.
host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
port: 9050,
user_password: None
})
);
assert_eq!(proxy_config.to_url(), "socks5://[::1]:9050".to_string());
let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap(); let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap();
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 9150, port: 9150,
user_password: Some(("foo".to_string(), "bar".to_string())) user_password: Some(("foo".to_string(), "bar".to_string()))
}) })
@@ -527,7 +550,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 9150, port: 9150,
user_password: Some(("foo".to_string(), "bar".to_string())) user_password: Some(("foo".to_string(), "bar".to_string()))
}) })
@@ -537,7 +560,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 80, port: 80,
user_password: None user_password: None
}) })
@@ -547,7 +570,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 1080, port: 1080,
user_password: None user_password: None
}) })
@@ -557,7 +580,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 1080, port: 1080,
user_password: None user_password: None
}) })
@@ -567,7 +590,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "my-proxy.example.org".to_string(), host: url::Host::Domain("my-proxy.example.org".to_string()),
port: 1080, port: 1080,
user_password: None user_password: None
}) })
@@ -584,37 +607,61 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Http(HttpConfig { ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 80, port: 80,
user_password: None user_password: None
}) })
); );
let proxy_config = ProxyConfig::from_url("http://[::1]").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Http(HttpConfig {
host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
port: 80,
user_password: None
})
);
assert_eq!(proxy_config.to_url(), "http://[::1]:80".to_string());
let proxy_config = ProxyConfig::from_url("http://127.0.0.1:80").unwrap(); let proxy_config = ProxyConfig::from_url("http://127.0.0.1:80").unwrap();
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Http(HttpConfig { ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 80, port: 80,
user_password: None user_password: None
}) })
); );
assert_eq!(proxy_config.to_url(), "http://127.0.0.1:80".to_string());
let proxy_config = ProxyConfig::from_url("http://[::1]:80").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Http(HttpConfig {
host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
port: 80,
user_password: None
})
);
assert_eq!(proxy_config.to_url(), "http://[::1]:80".to_string());
let proxy_config = ProxyConfig::from_url("http://127.0.0.1:443").unwrap(); let proxy_config = ProxyConfig::from_url("http://127.0.0.1:443").unwrap();
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Http(HttpConfig { ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 443, port: 443,
user_password: None user_password: None
}) })
); );
assert_eq!(proxy_config.to_url(), "http://127.0.0.1:443".to_string());
let proxy_config = ProxyConfig::from_url("http://my-proxy.example.org").unwrap(); let proxy_config = ProxyConfig::from_url("http://my-proxy.example.org").unwrap();
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Http(HttpConfig { ProxyConfig::Http(HttpConfig {
host: "my-proxy.example.org".to_string(), host: url::Host::Domain("my-proxy.example.org".to_string()),
port: 80, port: 80,
user_password: None user_password: None
}) })
@@ -631,7 +678,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Https(HttpConfig { ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 443, port: 443,
user_password: None user_password: None
}) })
@@ -641,7 +688,17 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Https(HttpConfig { ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 80,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("https://[::1]:80").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Https(HttpConfig {
host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
port: 80, port: 80,
user_password: None user_password: None
}) })
@@ -651,17 +708,29 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Https(HttpConfig { ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 443, port: 443,
user_password: None user_password: None
}) })
); );
assert_eq!(proxy_config.to_url(), "https://127.0.0.1:443".to_string());
let proxy_config = ProxyConfig::from_url("https://[::1]:443").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Https(HttpConfig {
host: url::Host::Ipv6(Ipv6Addr::LOCALHOST),
port: 443,
user_password: None
})
);
assert_eq!(proxy_config.to_url(), "https://[::1]:443".to_string());
let proxy_config = ProxyConfig::from_url("https://my-proxy.example.org").unwrap(); let proxy_config = ProxyConfig::from_url("https://my-proxy.example.org").unwrap();
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Https(HttpConfig { ProxyConfig::Https(HttpConfig {
host: "my-proxy.example.org".to_string(), host: url::Host::Domain("my-proxy.example.org".to_string()),
port: 443, port: 443,
user_password: None user_password: None
}) })

View File

@@ -126,9 +126,12 @@ pub async fn wrap_rustls<'a>(
let root_cert_store = let root_cert_store =
rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let mut config = rustls::ClientConfig::builder() let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(
.with_root_certificates(root_cert_store) rustls::crypto::aws_lc_rs::default_provider(),
.with_no_client_auth(); ))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_cert_store)
.with_no_client_auth();
config.alpn_protocols = if alpn.is_empty() { config.alpn_protocols = if alpn.is_empty() {
vec![] vec![]
} else { } else {

View File

@@ -51,7 +51,7 @@ impl rustls::client::danger::ServerCertVerifier for CustomCertificateVerifier {
let spki = parsed_certificate.subject_public_key_info(); let spki = parsed_certificate.subject_public_key_info();
let provider = rustls::crypto::ring::default_provider(); let provider = rustls::crypto::aws_lc_rs::default_provider();
if let ServerName::DnsName(dns_name) = server_name if let ServerName::DnsName(dns_name) = server_name
&& dns_name.as_ref().starts_with("_") && dns_name.as_ref().starts_with("_")
@@ -97,7 +97,7 @@ impl rustls::client::danger::ServerCertVerifier for CustomCertificateVerifier {
cert: &CertificateDer<'_>, cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct, dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
let provider = rustls::crypto::ring::default_provider(); let provider = rustls::crypto::aws_lc_rs::default_provider();
let supported_schemes = &provider.signature_verification_algorithms; let supported_schemes = &provider.signature_verification_algorithms;
rustls::crypto::verify_tls12_signature(message, cert, dss, supported_schemes) rustls::crypto::verify_tls12_signature(message, cert, dss, supported_schemes)
} }
@@ -108,13 +108,13 @@ impl rustls::client::danger::ServerCertVerifier for CustomCertificateVerifier {
cert: &CertificateDer<'_>, cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct, dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
let provider = rustls::crypto::ring::default_provider(); let provider = rustls::crypto::aws_lc_rs::default_provider();
let supported_schemes = &provider.signature_verification_algorithms; let supported_schemes = &provider.signature_verification_algorithms;
rustls::crypto::verify_tls13_signature(message, cert, dss, supported_schemes) rustls::crypto::verify_tls13_signature(message, cert, dss, supported_schemes)
} }
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> { fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
let provider = rustls::crypto::ring::default_provider(); let provider = rustls::crypto::aws_lc_rs::default_provider();
provider provider
.signature_verification_algorithms .signature_verification_algorithms
.supported_schemes() .supported_schemes()

View File

@@ -835,21 +835,16 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
{ {
can_info_msg = false; can_info_msg = false;
if mime_parser.pre_message == PreMessageMode::Post if mime_parser.pre_message == PreMessageMode::Post
&& let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid_orig).await? && let Some(msg) =
Message::load_by_rfc724_mid_optional(context, rfc724_mid_orig).await?
{ {
// The messsage is a post-message and pre-message exists. // The messsage is a post-message and pre-message exists.
// Assign status update to existing message because just received post-message will be trashed. // Assign status update to existing message because just received post-message will be trashed.
Some( Some(msg)
Message::load_from_db(context, msg_id)
.await
.context("Failed to load webxdc instance that we just checked exists")?,
)
} else { } else {
Some( Message::load_from_db_optional(context, insert_msg_id)
Message::load_from_db(context, insert_msg_id) .await
.await .context("Failed to load just created webxdc instance")?
.context("Failed to load just created webxdc instance")?,
)
} }
} else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) { } else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
if let Some(instance) = if let Some(instance) =
@@ -3795,13 +3790,17 @@ async fn apply_out_broadcast_changes(
} else if from_id == ContactId::SELF } else if from_id == ContactId::SELF
&& let Some(removed_id) = removed_id && let Some(removed_id) = removed_id
{ {
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id) if chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
.await?; .await?
{
better_msg.get_or_insert( better_msg.get_or_insert(
stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await, stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
); );
added_removed_id = Some(removed_id); added_removed_id = Some(removed_id);
} else {
info!(context, "No-op broadcast member removal message (TRASH).");
better_msg = Some("".to_string());
}
} }
} }
@@ -3875,17 +3874,20 @@ async fn apply_in_broadcast_changes(
} }
chat::delete_broadcast_secret(context, chat.id).await?; chat::delete_broadcast_secret(context, chat.id).await?;
if from_id == ContactId::SELF { let removed =
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
.await?;
if !removed {
info!(context, "No-op broadcast SELF-removal message (TRASH).");
better_msg = Some("".to_string());
} else if from_id == ContactId::SELF {
better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context)); better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context));
} else { } else {
better_msg.get_or_insert( better_msg.get_or_insert(
stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await, stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
); );
} }
send_event_chat_modified |= removed;
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
.await?;
send_event_chat_modified = true;
} else if !chat.is_self_in_chat(context).await? { } else if !chat.is_self_in_chat(context).await? {
chat::add_to_chat_contacts_table( chat::add_to_chat_contacts_table(
context, context,

View File

@@ -2346,7 +2346,7 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
transaction.execute( transaction.execute(
"UPDATE transports "UPDATE transports
SET entered_param=json_set(entered_param, '$.imap.folder', ?1), SET entered_param=json_set(entered_param, '$.imap.folder', ?1),
configured_param=json_set(configured_param', '$.imap_folder', ?1)", configured_param=json_set(configured_param, '$.imap_folder', ?1)",
(mvbox_folder,), (mvbox_folder,),
)?; )?;
} }

View File

@@ -18,7 +18,7 @@ use crate::config::Config;
use crate::constants::{Chattype, DC_VERSION_STR}; use crate::constants::{Chattype, DC_VERSION_STR};
use crate::contact::{Contact, ContactId, Origin, import_vcard, mark_contact_id_as_verified}; use crate::contact::{Contact, ContactId, Origin, import_vcard, mark_contact_id_as_verified};
use crate::context::Context; use crate::context::Context;
use crate::key::load_self_public_keyring; use crate::key::{DcKey, load_self_public_key};
use crate::log::LogExt; use crate::log::LogExt;
use crate::message::{Message, Viewtype}; use crate::message::{Message, Viewtype};
use crate::securejoin::QrInvite; use crate::securejoin::QrInvite;
@@ -33,7 +33,14 @@ const MESSAGE_STATS_UPDATE_INTERVAL_SECONDS: i64 = 4 * 60; // 4 minutes (less th
#[derive(Serialize)] #[derive(Serialize)]
struct Statistics { struct Statistics {
core_version: String, core_version: String,
number_of_transports: usize,
key_create_timestamps: Vec<u32>, key_create_timestamps: Vec<u32>,
number_of_keys: u32,
/// OpenPGP version of the key.
key_version: u8,
key_algorithm: String,
/// Size of the public key in bytes (encoded in binary, not base64).
pubkey_size: usize,
stats_id: String, stats_id: String,
is_chatmail: bool, is_chatmail: bool,
contact_stats: Vec<ContactStat>, contact_stats: Vec<ContactStat>,
@@ -345,11 +352,15 @@ async fn get_stats(context: &Context) -> Result<String> {
.get_config_u32(Config::StatsLastOldContactId) .get_config_u32(Config::StatsLastOldContactId)
.await?; .await?;
let key_create_timestamps: Vec<u32> = load_self_public_keyring(context) let self_public_key = load_self_public_key(context).await?;
// `key_create_timestamps` is a `Vec` for historical reasons,
// support for using multiple keys is being phased out.
let key_create_timestamps: Vec<u32> = vec![self_public_key.created_at().as_secs()];
let number_of_keys: u32 = context
.sql
.query_get_value("SELECT COUNT(*) FROM keypairs", ())
.await? .await?
.iter() .unwrap_or(0);
.map(|k| k.created_at().as_secs())
.collect();
let sending_enabled_timestamps = let sending_enabled_timestamps =
get_timestamps(context, "stats_sending_enabled_events").await?; get_timestamps(context, "stats_sending_enabled_events").await?;
@@ -358,7 +369,12 @@ async fn get_stats(context: &Context) -> Result<String> {
let stats = Statistics { let stats = Statistics {
core_version: DC_VERSION_STR.to_string(), core_version: DC_VERSION_STR.to_string(),
number_of_transports: context.count_transports().await?,
key_create_timestamps, key_create_timestamps,
number_of_keys,
key_version: self_public_key.primary_key.version().into(),
key_algorithm: format!("{:?}", self_public_key.algorithm()),
pubkey_size: DcKey::to_bytes(&self_public_key).len(),
stats_id: stats_id(context).await?, stats_id: stats_id(context).await?,
is_chatmail: context.is_chatmail().await?, is_chatmail: context.is_chatmail().await?,
contact_stats: get_contact_stats(context, last_old_contact).await?, contact_stats: get_contact_stats(context, last_old_contact).await?,

View File

@@ -595,3 +595,33 @@ async fn test_stats_enable_disable_timestamps() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cryptography_stats() -> Result<()> {
let alice = &TestContext::new_alice().await;
let stats = get_stats(alice).await.unwrap();
let stats: serde_json::Value = serde_json::from_str(&stats)?;
let number_of_transports: u64 = stats.get("number_of_transports").unwrap().as_u64().unwrap();
assert_eq!(number_of_transports, 1);
let key_version = stats.get("key_version").unwrap().as_u64().unwrap();
// Alice's key is v4
assert_eq!(key_version, 4);
let key_algorithm = stats.get("key_algorithm").unwrap().as_str().unwrap();
assert_eq!(key_algorithm, "EdDSALegacy");
let pubkey_size = stats.get("pubkey_size").unwrap().as_u64().unwrap();
assert_eq!(pubkey_size, 583);
crate::transport::add_pseudo_transport(alice, "alice@ten.testrun.org").await?;
let stats = get_stats(alice).await.unwrap();
let stats: serde_json::Value = serde_json::from_str(&stats)?;
let number_of_transports: u64 = stats.get("number_of_transports").unwrap().as_u64().unwrap();
assert_eq!(number_of_transports, 2);
Ok(())
}

View File

@@ -38,7 +38,7 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, self_fingerprint}; use crate::key::{self, DcKey, self_fingerprint};
use crate::log::warn; use crate::log::warn;
use crate::login_param::EnteredLoginParam; use crate::login_param::EnteredLoginParam;
use crate::message::{Message, MessageState, MsgId, update_msg_state}; use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::pgp::SeipdVersion; use crate::pgp::SeipdVersion;
use crate::receive_imf::{ReceivedMsg, receive_imf}; use crate::receive_imf::{ReceivedMsg, receive_imf};
@@ -692,10 +692,11 @@ ORDER BY id"
if !msg_has_pending_smtp_job(self, msg_id) if !msg_has_pending_smtp_job(self, msg_id)
.await .await
.expect("Failed to check for more jobs") .expect("Failed to check for more jobs")
{ && msg_id
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered) .set_delivered(self)
.await .await
.expect("failed to update message state"); .expect("MsgId::set_delivered")
{
self.sql self.sql
.execute( .execute(
"UPDATE msgs SET timestamp_sent=? WHERE id=?", "UPDATE msgs SET timestamp_sent=? WHERE id=?",
@@ -772,16 +773,19 @@ ORDER BY id"
.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,)) .execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))
.await .await
.expect("Delete smtp jobs"); .expect("Delete smtp jobs");
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered) if msg_id
.set_delivered(self)
.await .await
.expect("Update message state"); .expect("MsgId::set_delivered")
self.sql {
.execute( self.sql
"UPDATE msgs SET timestamp_sent=? WHERE id=?", .execute(
(time(), msg_id), "UPDATE msgs SET timestamp_sent=? WHERE id=?",
) (time(), msg_id),
.await )
.expect("Update timestamp_sent"); .await
.expect("Update timestamp_sent");
}
sent_msgs sent_msgs
} }

View File

@@ -565,6 +565,45 @@ async fn test_webxdc_updates_in_post_message_after_pre_message() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_updates_in_post_message_after_deleted_pre_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = alice.create_chat_id(bob).await;
let big_webxdc_app = big_webxdc_app().await?;
let mut alice_instance = Message::new(Viewtype::Webxdc);
alice_instance.set_file_from_bytes(alice, "test.xdc", &big_webxdc_app, None)?;
alice_instance.set_text("Test".to_string());
alice_chat_id
.set_draft(alice, Some(&mut alice_instance))
.await?;
alice
.send_webxdc_status_update(alice_instance.id, r#"{"payload":42, "info":"i"}"#)
.await?;
send_msg(alice, alice_chat_id, &mut alice_instance).await?;
let post_message = alice.pop_sent_msg().await;
let pre_message = alice.pop_sent_msg().await;
let bob_instance = bob.recv_msg(&pre_message).await;
assert_eq!(bob_instance.download_state, DownloadState::Available);
delete_msgs(bob, &[bob_instance.id]).await?;
bob.recv_msg_trash(&post_message).await;
// Deleted message stays trashed because of a tombstone.
assert!(
Message::load_from_db_optional(bob, bob_instance.id)
.await?
.is_none()
);
Ok(())
}
/// Tests receiving of a large webxdc post-message with updates attached /// Tests receiving of a large webxdc post-message with updates attached
/// to the the .xdc post-message when pre-message arrives later. /// to the the .xdc post-message when pre-message arrives later.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -41,6 +41,7 @@ async fn test_parse_receive_headers_integration() {
let raw = include_bytes!("../../test-data/message/mail_with_cc.txt"); let raw = include_bytes!("../../test-data/message/mail_with_cc.txt");
let expected = r"State: Fresh let expected = r"State: Fresh
Database ID: X
Message-ID: 2dfdbde7@example.org Message-ID: 2dfdbde7@example.org
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000 Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
@@ -50,6 +51,7 @@ Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25
let raw = include_bytes!("../../test-data/message/encrypted_with_received_headers.eml"); let raw = include_bytes!("../../test-data/message/encrypted_with_received_headers.eml");
let expected = "State: Fresh, Encrypted let expected = "State: Fresh, Encrypted
Database ID: X
Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000 Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
@@ -71,7 +73,10 @@ async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) {
// received time that depends on the test time which makes it impossible to // received time that depends on the test time which makes it impossible to
// compare with a static string // compare with a static string
let capped_result = &msg_info[msg_info.find("State").unwrap()..]; let capped_result = &msg_info[msg_info.find("State").unwrap()..];
assert_eq!(expected, capped_result); assert_eq!(
expected.replace("\nDatabase ID: X", &format!("\nDatabase ID: {msg_id}")),
capped_result
);
} }
#[test] #[test]

View File

@@ -252,7 +252,11 @@ impl fmt::Display for ConfiguredLoginParam {
write!(f, "{imap}")?; write!(f, "{imap}")?;
first = false; first = false;
} }
write!(f, "] smtp:[")?; write!(f, "]")?;
if let Some(folder) = &self.imap_folder {
write!(f, " folder:{folder:?}")?;
}
write!(f, " smtp:[")?;
let mut first = true; let mut first = true;
for smtp in &self.smtp { for smtp in &self.smtp {
if !first { if !first {

View File

@@ -34,7 +34,7 @@ async fn test_save_load_login_param() -> Result<()> {
}, },
user: "alice".to_string(), user: "alice".to_string(),
}], }],
imap_folder: None, imap_folder: Some("Folder".to_string()),
imap_user: "".to_string(), imap_user: "".to_string(),
imap_password: "foo".to_string(), imap_password: "foo".to_string(),
smtp: vec![ConfiguredServerLoginParam { smtp: vec![ConfiguredServerLoginParam {
@@ -56,7 +56,7 @@ async fn test_save_load_login_param() -> Result<()> {
.clone() .clone()
.save_to_transports_table(&t, &EnteredLoginParam::default(), time()) .save_to_transports_table(&t, &EnteredLoginParam::default(), time())
.await?; .await?;
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#; let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_folder":"Folder","imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
assert_eq!( assert_eq!(
t.sql t.sql
.query_get_value::<String>("SELECT configured_param FROM transports", ()) .query_get_value::<String>("SELECT configured_param FROM transports", ())
@@ -68,6 +68,14 @@ async fn test_save_load_login_param() -> Result<()> {
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap(); let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(param, loaded); assert_eq!(param, loaded);
let formatted = format!(" {loaded}");
assert!(formatted.contains(" ***@example.org"));
assert!(formatted.contains(" imap:[imap.example.com:123:starttls]"));
assert!(formatted.contains(" folder:\"Folder\""));
assert!(formatted.contains(" smtp:[smtp.example.com:456:tls]"));
assert!(formatted.contains(" provider:none"));
assert!(formatted.contains(" cert_strict"));
// Legacy ConfiguredImapCertificateChecks config is ignored // Legacy ConfiguredImapCertificateChecks config is ignored
t.set_config(Config::ConfiguredImapCertificateChecks, Some("999")) t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
.await?; .await?;

View File

@@ -0,0 +1,63 @@
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="18b255ee53144064_9da6abff99cb33df_3854fea559a18923"
MIME-Version: 1.0
From: <alice@example.org>
To: "hidden-recipients": ;
Subject: [...]
Date: Fri, 22 May 2026 22:48:16 +0000
Message-ID: <3a5ae0d2-be4b-4463-8011-ab4a354ee690@localhost>
Chat-Version: 1.0
--18b255ee53144064_9da6abff99cb33df_3854fea559a18923
Content-Type: application/pgp-encrypted; charset="utf-8"
Content-Transfer-Encoding: 7bit
Version: 1
--18b255ee53144064_9da6abff99cb33df_3854fea559a18923
Content-Type: application/octet-stream; charset="utf-8"
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
wUcGABIBB0BNBleU/G9GwCpizow72+5GANtiX+F7y/gI45x7MJLpFiBi+7OqwdFk
dmINorerXmnk1jR7TEppDYhjCBRWaCXHJsHARQYAAQf/Zj43ZUA3rtGS6Nq9vUcc
0L6knU0ukLc3KGTExQXu4ktiPT0RIjQ11wtSGlS75Bj7tu3syDf2/oQXi7eUh35T
IULpBQ5Pnpk6h26GX9WbRYR5zmBNnlyppqJcaYueQzPX1gvybJ4cwfOmnMdUqzFr
D9BR/YeyrBETsGKR+UfH167xgGY8d0ev8Tt4X01muiyrwLVqfoGCRUwjQFUY+ClY
9yuCrNDnu8fHY/WRTKXoDkA8Qn9/6TdJM7G41wOaJYbWn4R8WqjqoQNdUYwL2cBg
Pj1SNgaTaLMMH8YEtlRUN3JHiiqaBVIJIKBduOXnVEovf8FVXlmqX8DARLvqFCk2
LdLEjQIHAgdxw8asmmKOpatAEwFHuVwkWe/ZbRhu3D1V38BzZEuSLHzQ/nWbWWVP
GRmcZPmpusvRGX5W4zC6ybr5jePkW1dEqpgFFeoywz5REQxnl2QDPMHbWRFmC+q5
FSA/zacYGIKmU761xWc1raaosN4uzf8ujJA8H+TQT4AKu4K2nzSMFpUl05sNToGN
FdCNAvsnM9/PAraE3pju6YK+l4Woo/MLVEkrDEcLYEC/TrbNCJ6cnx/3uU+9gZyp
ntXzwOJIIQ5el2qcSQuzec931Lr4UuX4k0FUKeLS24EiYid4K59iPHgKJn6E7grb
34bxbQXkIMug0nTBKuAOTv7MFF4/rzn5deAUXpKty5zYLjqGApo4o5nnEvY9inXP
3489C77lCTLuXgkASrYhqTZcFT6ewaLNJxKRQgLB/V8BY4DrckA/If31z57EK2Ju
5kXpUfZKw/qjnHELLLSmYqx0Sd6w37d0x4LxHIjnpBEmsjWagM9fhiBJyaitNfCz
hBEUAQoO2DlqrZMwAhKxADH8WUbXOhgXcj68NM185A+/UzMbbdV0cmLKynH3tlLX
d16ZyriiRcllSuISXYLTsUwlQeA0zIyuYItzk7bGkw/2k4AmxFAFZjZPvaHOrt98
BapEkxYp8wyhXdloknJ687E5Oa4UTsMKzRTrXRiPhVNstYcYnde4fZs0cUX0JJqE
bZrR+T8biflfGWidc0D0cSd2rx6RryvykFBzvZJH4zqNaHap3w5F0miZWfrePQ8Y
ljPCb162x3SAHc5RzhHH7tq4YatWlnncSAuGkZA7XMqBgFbIRdY2z+Hv5AQLj563
OHD0Dc0pXUZYcHVdJnY4svuiekjEYLaGzjklULBsQo4KQXb8i3SufS8JUwypwAT1
BzMWYQrT6cv3IQHtF9Yys6Z3ngYs9UoCIyqPHYFZYV8Y64injpBFtiY6kd0zvm/3
5D5c0DcmI6kq0LNNNzZg5F0oMoDvyny1pI5/IGxS1H3OyBNJlNZkftMadyvDydY7
khCjWzB9mTStrmeYVqJof/NyfMWJMa0NtJrqW+730FYnld95cGzf3gnRsTYd/CgD
vXVcj5F2/fxsDUekmMMAYBB3DCBv0yCJJfqaTgQKcwngOL0+kuGVnjT8qfjf0FQR
yzImEPJFTQFe+mpRALBoMSDRpzja6ZkU/AIOTT/xy1ZkfpLU/TpNk7XS6InsIjdH
wBzfPvDUV7hHWwgILE+DTlebZVEuVBuoKTM72paMomYzskTkCGnIkTxioA1SyJxJ
eOsSlNC30Le6rcbvz+kl+H7cW2RGZuCUub/UtCMlcdjlzEVAjwXgQZgys2mAwLPx
n61I6rO+eEphXr5F/S6/0NwomR/02FY5AmqeViKC0G6/bDJOO374vU3bix28xqeB
yYYeO+ZymhjqA7tLTEjrkrCh0vo1WoKSAWbhVqM9YUzAeusqyndZ2uLbTgCEayER
MviI27DrXTDpExV9qLyELpo0z18kwoFScJ5ULXDN4lpwfsCeg3ThvBJD86V+4faT
4neKUwzasjYvuzKqSRV4j89I5Bs1g8ERaQr3VZ4BnyjKxQ91eLsMDD+LMx2QW2lB
HPUTWc4qmSAItogwZRxGUj+dASjcuGwqVhabUxWpxPsVGdOp6D3au1M3k7EuvLLa
NmnDDyNRSU8d7Mc/Tp9swfPKd2ItI4cREXTpyu53ayzSD5/7LaDtpHUASdaDLgDv
2evlUTBRk2a8+trbhE0oy8zzRdbgiNrwcegGM+y5BYCdV5Gooerk4g8pnULtKT1R
VgWMKvXvFduvQsfXW+PKChw=
=EBeW
-----END PGP MESSAGE-----
--18b255ee53144064_9da6abff99cb33df_3854fea559a18923--