mirror of
https://github.com/chatmail/core.git
synced 2026-07-23 03:13:06 +03:00
Compare commits
22 Commits
link2xt/re
...
v2.55.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35d7b4e21c | ||
|
|
03725c5f32 | ||
|
|
a0c96f4ce1 | ||
|
|
b432d08aa8 | ||
|
|
85db29bdfb | ||
|
|
23c216c84b | ||
|
|
d34dd40aa0 | ||
|
|
ebd1c20997 | ||
|
|
0c40b52ec8 | ||
|
|
aadd11a771 | ||
|
|
17b32a17f5 | ||
|
|
355ab8b3bc | ||
|
|
e2b67d20fb | ||
|
|
0560c13ae5 | ||
|
|
1a24bf612a | ||
|
|
492741ac60 | ||
|
|
d940f96a5c | ||
|
|
e5c920f1a1 | ||
|
|
5d0e5efc94 | ||
|
|
94fc4f7550 | ||
|
|
ce2d237ffd | ||
|
|
f2d11305bd |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.97.0
|
||||
RUST_VERSION: 1.97.1
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
cache-bin: false
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@9bcaee1dcae34154180f412e2fa69355a7cda9f6
|
||||
uses: taiki-e/install-action@50414676f9f5d50a65992c6dd2ed02641263226c
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
|
||||
9
.github/workflows/deltachat-rpc-server.yml
vendored
9
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -513,15 +513,12 @@ jobs:
|
||||
deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
package-manager-cache: false # never use caching in release builds
|
||||
|
||||
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||
if: github.event_name == 'release'
|
||||
|
||||
10
.github/workflows/jsonrpc-client-npm-package.yml
vendored
10
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -22,15 +22,13 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
package-manager-cache: false # never use caching in release builds
|
||||
|
||||
- name: Install dependencies without running scripts
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
|
||||
4
.github/workflows/jsonrpc.yml
vendored
4
.github/workflows/jsonrpc.yml
vendored
@@ -20,10 +20,10 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 18.x
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 24
|
||||
- name: Add Rust cache
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
|
||||
2
.github/workflows/upload-docs.yml
vendored
2
.github/workflows/upload-docs.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: 24
|
||||
- name: npm install
|
||||
run: npm install
|
||||
- name: npm run build
|
||||
|
||||
108
CHANGELOG.md
108
CHANGELOG.md
@@ -1,5 +1,111 @@
|
||||
# Changelog
|
||||
|
||||
## [2.55.0] - 2026-07-20
|
||||
|
||||
Minor release to fix CI because releasing 2.54.0 failed.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Node version to 24.
|
||||
|
||||
## [2.54.0] - 2026-07-20
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Deprecate `is_chatmail`.
|
||||
- UIs should not behave differently for chatmail relays than for classical email servers; most usages of `is_chatmail` can be replaced by `force_encryption`.
|
||||
- [**breaking**] `delete_transport()` must not be used by UIs anymore. Instead, `set_transport_unpublished()` must be called when a user clicks on "Remove".
|
||||
- [**breaking**] `list_transports()` doesn't return unpublished relays anymore.
|
||||
- UIs should use `list_transports()` rather than `list_transports_ex()`, because unpublished transports count as removed from the user point of view, and should not be shown in the relay list anymore.
|
||||
- deltachat-rpc-client: add `Account.set_transport_unpublished()`.
|
||||
- Add `MsgReadCountChanged` event.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Implement support for populating and maintaining a list of default relays ([#8341](https://github.com/chatmail/core/pull/8341)).
|
||||
- Remove hidden relays automatically ([#8402](https://github.com/chatmail/core/pull/8402)).
|
||||
- Automatically remove oldest unpublished relay in order to make space when the user wants to add more; don't allow more than 5 relays overall ([#8428](https://github.com/chatmail/core/pull/8428)).
|
||||
- Add silent group changes messages as InNoticed, not InSeen.
|
||||
- Remove `?emailaddress` argument from autoconfig URL that is not using a dedicated domain.
|
||||
- Remove `imap::Session::sync_seen_flags()` ([#7742](https://github.com/chatmail/core/pull/7742)).
|
||||
- Use CAPABILITY response code if IMAP LOGIN command returns it.
|
||||
- Increase max idle timeout for iroh backup receiver to 60 seconds.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Request MDNs for resent channel messages.
|
||||
- Make pre-messages w/o text want MDNs ([#8004](https://github.com/chatmail/core/pull/8004)).
|
||||
- Make truncated edited messages have HTML for receivers ([#8249](https://github.com/chatmail/core/pull/8249)).
|
||||
- Un-escape message footer marks in full messages (`get_html`) ([#8427](https://github.com/chatmail/core/pull/8427)).
|
||||
- Hide synced chat if we only know its visibility ([#8343](https://github.com/chatmail/core/pull/8343)).
|
||||
- Tombstone MDN before sending it ([#8252](https://github.com/chatmail/core/pull/8252)).
|
||||
- Recreate `imap_markseen` with `PRIMARY KEY` constraint.
|
||||
- Rerun the full securejoin protocol if the address was outdated ([#8358](https://github.com/chatmail/core/pull/8358)).
|
||||
- Return early from `receive_imf` to not tombstone Iroh-Node-Addr message if webxdc instance isn't found ([#8372](https://github.com/chatmail/core/pull/8372)).
|
||||
- Replace `last_added_location_id` with `last_added_location_timestamp`.
|
||||
- Do not put locations into pre-messages.
|
||||
- RUSTSEC-2026-0204 ([#8403](https://github.com/chatmail/core/pull/8403)).
|
||||
- Ensure public key signatures are not in the past compared to the public key.
|
||||
- Do not bubble up errors in IMAP candidate loop.
|
||||
- Do not log errors if full message is not available on any transport.
|
||||
- Apply reactions that arrived before the message at later time ([#8415](https://github.com/chatmail/core/pull/8415)).
|
||||
|
||||
### Performance
|
||||
|
||||
- Add timestamp to `msgs_index7` and speed up `Chatlist::try_load()` ([#7848](https://github.com/chatmail/core/pull/7848)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.97.1.
|
||||
- rrsync prepends the restricted upload path, we need to leave it out ([#8405](https://github.com/chatmail/core/pull/8405)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update STYLE.md: macros should be used only when necessary ([#8410](https://github.com/chatmail/core/pull/8410)).
|
||||
- `create_group_chat_unencrypted()` may lead to chat split on the first device.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Deprecate unused `SkipAutocrypt` param.
|
||||
- Remove commented out `RenderedEmail.envelope`.
|
||||
- Remove the ability to send messages with non-standard header protection.
|
||||
- Make `crate::pgp::symm_encrypt_message` non-async.
|
||||
- Move `ensure_secret_key_exists` into key.rs.
|
||||
- Improve comment ([#8366](https://github.com/chatmail/core/pull/8366)).
|
||||
- Remove `set_modseq()` function.
|
||||
- Remove unnecessary reference in format string.
|
||||
- Label the loop iterating over the candidates.
|
||||
- Remove `GROUP BY c.id` from chatlist queries.
|
||||
|
||||
### Tests
|
||||
|
||||
- securejoin: Check that "vc-{,request-}pubkey" messages don't contain displayname.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.54.0-dev.
|
||||
- deps: bump taiki-e/install-action from 2.81.1 to 2.81.8.
|
||||
- deps: bump taiki-e/install-action from 2.81.8 to 2.81.11.
|
||||
- update rPGP from 0.19.0 to 0.20.0.
|
||||
- update astral-tokio-tar from 0.6.2 to 0.6.3.
|
||||
- deps: bump anyhow to 1.0.103.
|
||||
- deps: bump actions/checkout from 6 to 7.
|
||||
- cargo: bump syn from 2.0.117 to 2.0.118.
|
||||
- cargo: bump quote from 1.0.45 to 1.0.46.
|
||||
- cargo: bump bytes from 1.11.1 to 1.12.0.
|
||||
- cargo: bump regex from 1.12.3 to 1.12.4.
|
||||
- cargo: bump log from 0.4.31 to 0.4.33.
|
||||
- cargo: bump hyper from 1.9.0 to 1.10.1.
|
||||
- deps: bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7.
|
||||
- cargo: bump chrono from 0.4.44 to 0.4.45.
|
||||
- update quick-xml to 0.41.0.
|
||||
- cargo: bump brotli from 8.0.2 to 8.0.4.
|
||||
- cargo: bump smallvec from 1.15.1 to 1.15.2.
|
||||
- deps: bump taiki-e/install-action from 2.81.11 to 2.82.6.
|
||||
- update yanked spin@0.9.8 and spin@0.10.0.
|
||||
- deps: bump taiki-e/install-action from 2.82.6 to 2.82.10.
|
||||
- update async-imap to 0.11.3.
|
||||
|
||||
## [2.53.0] - 2026-06-15
|
||||
|
||||
### Features / Changes
|
||||
@@ -8374,3 +8480,5 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.51.0]: https://github.com/chatmail/core/compare/v2.50.0..v2.51.0
|
||||
[2.52.0]: https://github.com/chatmail/core/compare/v2.51.0..v2.52.0
|
||||
[2.53.0]: https://github.com/chatmail/core/compare/v2.52.0..v2.53.0
|
||||
[2.54.0]: https://github.com/chatmail/core/compare/v2.53.0..v2.54.0
|
||||
[2.55.0]: https://github.com/chatmail/core/compare/v2.54.0..v2.55.0
|
||||
|
||||
28
Cargo.lock
generated
28
Cargo.lock
generated
@@ -271,9 +271,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a78dceaba06f029d8f4d7df20addd4b7370a30206e3926267ecda2915b0f3f66"
|
||||
checksum = "9a6728e0f7931b36d725ac234fcb02539e9f7888dbeaaa8a18d9ea5792181570"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-compression",
|
||||
@@ -1350,7 +1350,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1459,7 +1459,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1480,7 +1480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1496,7 +1496,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1525,7 +1525,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -2179,7 +2179,7 @@ dependencies = [
|
||||
"diatomic-waker",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"spin 0.9.8",
|
||||
"spin 0.9.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3342,7 +3342,7 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin 0.9.8",
|
||||
"spin 0.9.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5708,7 +5708,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"shadowsocks-crypto",
|
||||
"socket2 0.5.9",
|
||||
"spin 0.10.0",
|
||||
"spin 0.10.1",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-tfo",
|
||||
@@ -5895,15 +5895,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
@@ -44,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -590,7 +590,7 @@ impl CommandApi {
|
||||
|
||||
/// Immediately deletes a transport, potentially causing messages not to arrive.
|
||||
/// This must ONLY be used by the automated tests.
|
||||
/// UI implementations must use `set_transport_unpublished()` instead.
|
||||
/// UI implementations must use [`Self::set_transport_unpublished`] instead.
|
||||
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.delete_transport(&addr).await
|
||||
@@ -605,6 +605,14 @@ impl CommandApi {
|
||||
/// and self-sent messages are not sent there,
|
||||
/// so that we don't cause extra messages to the corresponding inbox,
|
||||
/// but can still receive messages from contacts who don't know our new transport addresses yet.
|
||||
///
|
||||
/// When more transports are added by [`Self::add_or_update_transport()`] or [`Self::add_transport_from_qr`],
|
||||
/// the least recently needed unpublished transport is automatically removed
|
||||
/// if this is necessary in order to stay below the maximum number of allowed relays.
|
||||
/// Also, unpublished transports that are not used to receive any new messages for a time defined by
|
||||
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`] are automatically removed.
|
||||
///
|
||||
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`]: deltachat::sql::UNPUBLISHED_TRANSPORT_KEEP_TIME
|
||||
async fn set_transport_unpublished(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.54.0-dev"
|
||||
"version": "2.55.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -133,6 +133,10 @@ class Account:
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
|
||||
def set_transport_unpublished(self, addr: str, unpublished: bool = True):
|
||||
"""Unpublish the transport."""
|
||||
self._rpc.set_transport_unpublished(self.id, addr, unpublished)
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
|
||||
@@ -237,10 +237,21 @@ def test_transport_limit(acfactory) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
account.delete_transport(second_addr)
|
||||
third_addr = account.list_transports()[2]["addr"]
|
||||
|
||||
# test that adding a transport after deleting one works again
|
||||
# test that adding a transport after unpublishing one works again
|
||||
account.set_transport_unpublished(second_addr)
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
# UIs are not expected to delete transports directly,
|
||||
# but we still test that adding a transport
|
||||
# after deleting one instead of unpublishing works.
|
||||
account.delete_transport(third_addr)
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory) -> None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.54.0-dev"
|
||||
"version": "2.55.0"
|
||||
}
|
||||
|
||||
444
flake.nix
444
flake.nix
@@ -29,7 +29,11 @@
|
||||
rustc = fenixToolchain;
|
||||
};
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
version = manifest.version;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
|
||||
|
||||
rustSrc = nix-filter.lib {
|
||||
root = ./.;
|
||||
@@ -79,7 +83,7 @@
|
||||
naersk'.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
@@ -87,22 +91,221 @@
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
};
|
||||
mkWin64RustPackage = packageName:
|
||||
let
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
pkgsWin64.stdenv.cc
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
mkWin64RustPackage = pkgs.callPackage ./nix/win64-package.nix {
|
||||
inherit naersk system fenixPkgs version;
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
"-L"
|
||||
"native=${pkgsWin64.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
mkWin32RustPackage = packageName:
|
||||
let
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
|
||||
# Get rid of MCF Gthread library.
|
||||
# See <https://github.com/NixOS/nixpkgs/issues/156343>
|
||||
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
|
||||
# for details.
|
||||
#
|
||||
# Use DWARF-2 instead of SJLJ for exception handling.
|
||||
winCC = pkgsWin32.buildPackages.wrapCC (
|
||||
(pkgsWin32.buildPackages.gcc-unwrapped.override
|
||||
({
|
||||
threadsCross = {
|
||||
model = "win32";
|
||||
package = null;
|
||||
};
|
||||
})).overrideAttrs (oldAttr: {
|
||||
configureFlags = oldAttr.configureFlags ++ [
|
||||
"--disable-sjlj-exceptions"
|
||||
"--with-dwarf2"
|
||||
];
|
||||
})
|
||||
);
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.nasm # aws-lc-sys requires it
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
"-L"
|
||||
"native=${pkgsWin32.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
LD = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
};
|
||||
|
||||
mkCrossRustPackage = arch: packageName:
|
||||
let
|
||||
crossTarget = arch2targets."${arch}";
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
androidAttrs = {
|
||||
armeabi-v7a = {
|
||||
cc = "armv7a-linux-androideabi21-clang";
|
||||
rustTarget = "armv7-linux-androideabi";
|
||||
};
|
||||
arm64-v8a = {
|
||||
cc = "aarch64-linux-android21-clang";
|
||||
rustTarget = "aarch64-linux-android";
|
||||
};
|
||||
x86 = {
|
||||
cc = "i686-linux-android21-clang";
|
||||
rustTarget = "i686-linux-android";
|
||||
};
|
||||
x86_64 = {
|
||||
cc = "x86_64-linux-android21-clang";
|
||||
rustTarget = "x86_64-linux-android";
|
||||
};
|
||||
};
|
||||
|
||||
mkWin32RustPackage = pkgs.callPackage ./nix/win32-package.nix {
|
||||
inherit naersk system fenixPkgs version;
|
||||
};
|
||||
mkAndroidRustPackage = arch: packageName:
|
||||
let
|
||||
rustTarget = androidAttrs.${arch}.rustTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
|
||||
targetCcName = androidAttrs.${arch}.cc;
|
||||
targetCc = "${targetToolchain}/bin/${targetCcName}";
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
mkCrossRustPackage = pkgs.callPackage ./nix/cross-rust-package.nix {
|
||||
inherit nixpkgs arch2targets naersk fenixPkgs system rustSrc version;
|
||||
};
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
mkAndroidRustPackage = pkgs.callPackage ./nix/android-package.nix {
|
||||
inherit naersk fenixPkgs system rustSrc android version;
|
||||
};
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
};
|
||||
|
||||
mkAndroidPackages = arch:
|
||||
let
|
||||
@@ -112,7 +315,32 @@
|
||||
"deltachat-rpc-server-${arch}-android" = rpc-server;
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}-android-wheel" =
|
||||
mkWheel { inherit rpc-server; arch = "${arch}-android"; };
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-android-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
|
||||
python3 scripts/wheel-rpc-server.py ${arch}-android tmp/deltachat-rpc-server
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
};
|
||||
|
||||
mkRustPackages = arch:
|
||||
@@ -122,10 +350,34 @@
|
||||
{
|
||||
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}" = rpc-server;
|
||||
"deltachat-rpc-server-${arch}-wheel" = mkWheel { inherit rpc-server; arch = "${arch}"; };
|
||||
"deltachat-rpc-server-${arch}-wheel" =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
|
||||
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
};
|
||||
|
||||
mkWheel = pkgs.callPackage ./nix/wheel.nix { inherit nix-filter version; root = ./.; };
|
||||
in
|
||||
{
|
||||
formatter = pkgs.nixpkgs-fmt;
|
||||
@@ -149,29 +401,116 @@
|
||||
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win64-wheel =
|
||||
mkWheel { rpc-server = deltachat-rpc-server-win64; arch = "win64"; binaryName = "deltachat-rpc-server.exe"; };
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-win64-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
deltachat-rpc-server-win64
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
|
||||
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
|
||||
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win32-wheel =
|
||||
mkWheel
|
||||
{ rpc-server = deltachat-rpc-server-win32; arch = "win32"; binaryName = "deltachat-rpc-server.exe"; };
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-win32-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
deltachat-rpc-server-win32
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
|
||||
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
# Run `nix build .#docs` to get C docs generated in `./result/`.
|
||||
docs = pkgs.callPackage ./nix/c-docs.nix { inherit version; };
|
||||
docs =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [ pkgs.doxygen ];
|
||||
buildPhase = ''scripts/run-doxygen.sh'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
|
||||
};
|
||||
|
||||
libdeltachat = pkgs.callPackage ./nix/libdeltachat.nix {
|
||||
inherit fenixToolchain rustSrc cargoLock fenixPkgs version;
|
||||
};
|
||||
libdeltachat =
|
||||
let
|
||||
rustPlatform = (pkgs.makeRustPlatform {
|
||||
cargo = fenixToolchain;
|
||||
rustc = fenixToolchain;
|
||||
});
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
version = manifest.version;
|
||||
src = rustSrc;
|
||||
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
|
||||
|
||||
deltachat-rpc-client = pkgs.callPackage ./nix/deltachat-rpc-client.nix {
|
||||
inherit version;
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.cmake
|
||||
rustPlatform.cargoSetupHook
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
substituteInPlace $out/include/deltachat.h \
|
||||
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
|
||||
'';
|
||||
};
|
||||
|
||||
deltachat-rpc-client =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-rpc-client";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.imap-tools
|
||||
];
|
||||
};
|
||||
|
||||
deltachat-python =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-python";
|
||||
inherit version;
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./python;
|
||||
format = "pyproject";
|
||||
buildInputs = [
|
||||
@@ -189,12 +528,51 @@
|
||||
pkgs.python3Packages.requests
|
||||
];
|
||||
};
|
||||
python-docs = pkgs.callPackage ./nix/python-docs.nix {
|
||||
inherit deltachat-python deltachat-rpc-client version;
|
||||
};
|
||||
python-docs =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
buildInputs = [
|
||||
deltachat-python
|
||||
deltachat-rpc-client
|
||||
pkgs.python3Packages.breathe
|
||||
pkgs.python3Packages.sphinx-rtd-theme
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.sphinx ];
|
||||
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
|
||||
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = import ./nix/shell.nix { inherit nixpkgs fenix system; };
|
||||
devShells.default =
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
"rustc"
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
(python3.withPackages (pypkgs: with pypkgs; [
|
||||
tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
{ pkgs, naersk, fenixPkgs, system, version, rustSrc, android }:
|
||||
arch: packageName:
|
||||
let
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
|
||||
androidAttrs = {
|
||||
armeabi-v7a = {
|
||||
cc = "armv7a-linux-androideabi21-clang";
|
||||
rustTarget = "armv7-linux-androideabi";
|
||||
};
|
||||
arm64-v8a = {
|
||||
cc = "aarch64-linux-android21-clang";
|
||||
rustTarget = "aarch64-linux-android";
|
||||
};
|
||||
x86 = {
|
||||
cc = "i686-linux-android21-clang";
|
||||
rustTarget = "i686-linux-android";
|
||||
};
|
||||
x86_64 = {
|
||||
cc = "x86_64-linux-android21-clang";
|
||||
rustTarget = "x86_64-linux-android";
|
||||
};
|
||||
};
|
||||
|
||||
rustTarget = androidAttrs.${arch}.rustTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
|
||||
targetCcName = androidAttrs.${arch}.cc;
|
||||
targetCc = "${targetToolchain}/bin/${targetCcName}";
|
||||
in
|
||||
naersk-lib.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${targetCc}"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{ pkgs, version }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
inherit version;
|
||||
src = pkgs.lib.cleanSource ../.;
|
||||
nativeBuildInputs = [ pkgs.doxygen ];
|
||||
buildPhase = ''scripts/run-doxygen.sh'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{ pkgs, nixpkgs, arch2targets, naersk, fenixPkgs, system, rustSrc, version }:
|
||||
arch: packageName:
|
||||
let
|
||||
crossTarget = arch2targets."${arch}";
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetCc = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
in
|
||||
naersk-lib.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${targetCc}"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{ pkgs, version }:
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-rpc-client";
|
||||
inherit version;
|
||||
src = pkgs.lib.cleanSource ../deltachat-rpc-client;
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.imap-tools
|
||||
];
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{ pkgs, fenixToolchain, rustSrc, cargoLock, fenixPkgs, version }:
|
||||
let
|
||||
rustPlatform = (pkgs.makeRustPlatform {
|
||||
cargo = fenixToolchain;
|
||||
rustc = fenixToolchain;
|
||||
});
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
inherit version;
|
||||
src = rustSrc;
|
||||
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.cmake
|
||||
rustPlatform.cargoSetupHook
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
substituteInPlace $out/include/deltachat.h \
|
||||
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
|
||||
'';
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{ pkgs, version, deltachat-python, deltachat-rpc-client }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
inherit version;
|
||||
src = pkgs.lib.cleanSource ../.;
|
||||
buildInputs = [
|
||||
deltachat-python
|
||||
deltachat-rpc-client
|
||||
pkgs.python3Packages.breathe
|
||||
pkgs.python3Packages.sphinx-rtd-theme
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.sphinx ];
|
||||
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
|
||||
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{ nixpkgs, fenix, system }:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
"rustc"
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
(python3.withPackages (pypkgs: [
|
||||
pypkgs.tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{ pkgs, nix-filter, version, root }:
|
||||
{ rpc-server, arch, binaryName ? "deltachat-rpc-server" }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-wheel";
|
||||
inherit version;
|
||||
src = nix-filter.lib {
|
||||
inherit root;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/${binaryName} tmp/${binaryName}
|
||||
python3 scripts/wheel-rpc-server.py ${arch} tmp/${binaryName}
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
{ pkgs, naersk, fenixPkgs, system, version }:
|
||||
packageName:
|
||||
let
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
|
||||
# Get rid of MCF Gthread library.
|
||||
# See <https://github.com/NixOS/nixpkgs/issues/156343>
|
||||
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
|
||||
# for details.
|
||||
#
|
||||
# Use DWARF-2 instead of SJLJ for exception handling.
|
||||
winCC = pkgsWin32.buildPackages.wrapCC (
|
||||
(pkgsWin32.buildPackages.gcc-unwrapped.override
|
||||
({
|
||||
threadsCross = {
|
||||
model = "win32";
|
||||
package = null;
|
||||
};
|
||||
})).overrideAttrs (oldAttr: {
|
||||
configureFlags = oldAttr.configureFlags ++ [
|
||||
"--disable-sjlj-exceptions"
|
||||
"--with-dwarf2"
|
||||
];
|
||||
})
|
||||
);
|
||||
targetCc = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
in
|
||||
naerskWin.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ../.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.nasm # aws-lc-sys requires it
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${targetCc}"
|
||||
"-L"
|
||||
"native=${pkgsWin32.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{ pkgs, naersk, fenixPkgs, system, version }:
|
||||
packageName:
|
||||
let
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
targetCc = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
in
|
||||
naerskWin.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ../.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
pkgsWin64.stdenv.cc
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${targetCc}"
|
||||
"-L"
|
||||
"native=${pkgsWin64.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
}
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.55.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-06-15
|
||||
2026-07-20
|
||||
108
src/chatlist.rs
108
src/chatlist.rs
@@ -115,35 +115,40 @@ impl Chatlist {
|
||||
ChatId::new(0)
|
||||
};
|
||||
|
||||
// select with left join and minimum:
|
||||
//
|
||||
// - the inner select must use `hidden` and _not_ `m.hidden`
|
||||
// which would refer the outer select and take a lot of time
|
||||
// - `GROUP BY` is needed several messages may have the same
|
||||
// timestamp
|
||||
// - the list starts with the newest chats
|
||||
macro_rules! last_visible_msg_id_in {
|
||||
($chat_id:expr) => {
|
||||
concat!(
|
||||
"
|
||||
(SELECT id FROM msgs WHERE
|
||||
-- state=`OutDraft`.
|
||||
state=19 AND hidden=1 AND chat_id=",
|
||||
$chat_id,
|
||||
" OR
|
||||
-- `InFresh`...`OutDelivered` inclusive, except `OutDraft`.
|
||||
state IN (10,13,16,20,24,26) AND hidden=0 AND chat_id=",
|
||||
$chat_id,
|
||||
"
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)"
|
||||
)
|
||||
};
|
||||
}
|
||||
// The list starts with the newest chats.
|
||||
//
|
||||
// The query shows messages from blocked contacts in
|
||||
// groups. Otherwise it would be hard to follow conversations.
|
||||
let ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map_vec(
|
||||
context.sql.query_map_vec(concat!(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
AND m.id=", last_visible_msg_id_in!("c.id"), "
|
||||
WHERE c.id>9
|
||||
AND c.blocked!=1
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp)
|
||||
ORDER BY c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"),
|
||||
(query_contact_id, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
} else if flag_archived_only {
|
||||
@@ -154,22 +159,21 @@ impl Chatlist {
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
concat!(
|
||||
"
|
||||
SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
AND m.id=",
|
||||
last_visible_msg_id_in!("c.id"),
|
||||
"
|
||||
WHERE c.id>9
|
||||
AND c.blocked!=1
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft,),
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"
|
||||
),
|
||||
(),
|
||||
process_row,
|
||||
)
|
||||
.await?
|
||||
@@ -188,24 +192,18 @@ impl Chatlist {
|
||||
let str_like_cmd = format!("%{}%", query.to_lowercase());
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
.query_map_vec(concat!(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND m.id=", last_visible_msg_id_in!("c.id"), "
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND c.blocked!=1
|
||||
AND IFNULL(c.name_normalized,c.name) LIKE ?3
|
||||
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
AND IFNULL(c.name_normalized,c.name) LIKE ?
|
||||
AND (NOT ? OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ? AND hidden=0))
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"),
|
||||
(skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
process_row,
|
||||
)
|
||||
.await?
|
||||
@@ -237,25 +235,19 @@ impl Chatlist {
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
};
|
||||
context.sql.query_map(
|
||||
context.sql.query_map(concat!(
|
||||
"SELECT c.id, c.type, c.param, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
AND m.id=", last_visible_msg_id_in!("c.id"), "
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?
|
||||
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"),
|
||||
(
|
||||
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
|
||||
skip_id, ChatVisibility::Archived,
|
||||
Chattype::Group, ContactId::SELF,
|
||||
sort_id_up, ChatVisibility::Pinned,
|
||||
),
|
||||
@@ -264,23 +256,17 @@ impl Chatlist {
|
||||
).await?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
context.sql.query_map_vec(
|
||||
context.sql.query_map_vec(concat!(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
AND m.id=", last_visible_msg_id_in!("c.id"), "
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND (c.blocked=0 OR c.blocked=2)
|
||||
AND NOT c.archived=?
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC"),
|
||||
(skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
};
|
||||
|
||||
186
src/configure.rs
186
src/configure.rs
@@ -45,9 +45,10 @@ use crate::transport::{
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{chat, provider};
|
||||
|
||||
/// Maximum number of relays
|
||||
/// see <https://github.com/chatmail/core/issues/7608>
|
||||
pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
|
||||
/// Maximum number of relays.
|
||||
///
|
||||
/// See <https://github.com/chatmail/core/issues/7608>.
|
||||
pub(crate) const MAX_RELAYS: usize = 5;
|
||||
|
||||
/// Hard-coded candidates for default relays.
|
||||
/// In the future, we want to use it during onboarding;
|
||||
@@ -231,8 +232,8 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Immediately deletes a transport, potentially causing messages not to arrive.
|
||||
/// This must ONLY be used by the automated tests.
|
||||
/// UI implementations must use `set_transport_unpublished()` instead.
|
||||
/// This must ONLY be used internally and by the automated tests.
|
||||
/// UI implementations must use [`Self::set_transport_unpublished`] instead.
|
||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||
let now = time();
|
||||
let removed_transport_id = self
|
||||
@@ -291,6 +292,12 @@ impl Context {
|
||||
/// and self-sent messages are not sent there,
|
||||
/// so that we don't cause extra messages to the corresponding inbox,
|
||||
/// but can still receive messages from contacts who don't know our new transport addresses yet.
|
||||
///
|
||||
/// When more transports are added by [`Self::add_or_update_transport()`] or [`Self::add_transport_from_qr`],
|
||||
/// the least recently needed unpublished transport is automatically removed
|
||||
/// if this is necessary in order to stay below the maximum number of allowed relays.
|
||||
/// Also, unpublished transports that are not used to receive any new messages for a time defined by
|
||||
/// `UNPUBLISHED_TRANSPORT_KEEP_TIME` are automatically removed.
|
||||
pub async fn set_transport_unpublished(&self, addr: &str, unpublished: bool) -> Result<()> {
|
||||
self.sql
|
||||
.transaction(|trans| {
|
||||
@@ -322,25 +329,15 @@ impl Context {
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
if old_addr.is_some()
|
||||
&& !self
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(¶m.addr,),
|
||||
)
|
||||
.await?
|
||||
&& self
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM transports", ())
|
||||
.await?
|
||||
>= MAX_TRANSPORT_RELAYS
|
||||
{
|
||||
bail!(
|
||||
"You have reached the maximum number of relays ({}).",
|
||||
MAX_TRANSPORT_RELAYS
|
||||
if !self
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(¶m.addr,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
self.try_make_space_for_new_relay().await?;
|
||||
}
|
||||
|
||||
let provider = match configure(self, param).await {
|
||||
@@ -364,6 +361,42 @@ impl Context {
|
||||
on_configure_completed(self, provider).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function is called before adding a new relay.
|
||||
/// If the maximum number of relays ([`MAX_RELAYS`]) is already reached,
|
||||
/// then it tries to make space by removing an unpublished relay.
|
||||
/// If there are multiple unpublished relays,
|
||||
/// the one that hasn't received a message for longest is removed.
|
||||
/// If there are no unpublished relays, an error is returned.
|
||||
///
|
||||
/// Note that eviction happens before we know that a new relay works,
|
||||
/// which is a trade-off we made in favor of implementation complexity.
|
||||
async fn try_make_space_for_new_relay(&self) -> Result<()> {
|
||||
if self.count_transports().await? >= MAX_RELAYS {
|
||||
// Try to automatically remove the unpublished transport that wasn't used for the longest time:
|
||||
if let Some(addr) = self
|
||||
.sql
|
||||
.query_get_value::<String>(
|
||||
"SELECT addr FROM transports WHERE is_published=0
|
||||
ORDER BY last_rcvd_timestamp, add_timestamp LIMIT 1",
|
||||
(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
info!(
|
||||
self,
|
||||
"Auto-deleting relay {addr} to make space for new relay."
|
||||
);
|
||||
self.delete_transport(&addr).await?;
|
||||
}
|
||||
|
||||
if self.count_transports().await? >= MAX_RELAYS {
|
||||
// Apparently, all the transports are published
|
||||
bail!("You have reached the maximum number of relays ({MAX_RELAYS})");
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_configure_completed(
|
||||
@@ -810,10 +843,14 @@ pub enum Error {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::login_param::EnteredImapLoginParam;
|
||||
use crate::sql::update_transport_last_rcvd_timestamp;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_panic_on_bad_credentials() {
|
||||
@@ -844,4 +881,107 @@ mod tests {
|
||||
assert_eq!(configured_param.smtp_user, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_make_place_for_new_relay() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Setting ConfiguredAddr on an unconfigured account creates a pseudo primary transport
|
||||
t.set_config(Config::ConfiguredAddr, Some("primary@example.org"))
|
||||
.await?;
|
||||
|
||||
// Test that try_make_place_for_new_relay() doesn't do anything when we're below the limit
|
||||
assert_eq!(t.count_transports().await?, 1);
|
||||
t.try_make_space_for_new_relay().await?;
|
||||
assert_eq!(t.count_transports().await?, 1);
|
||||
|
||||
for i in 0..(MAX_RELAYS - 2) {
|
||||
add_pseudo_transport(&t, &format!("transport{i}@example.org")).await?;
|
||||
}
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS - 1);
|
||||
t.try_make_space_for_new_relay().await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS - 1);
|
||||
|
||||
// Test that try_make_place_for_new_relay() removes the unpublished transport
|
||||
// when we're at the limit
|
||||
add_pseudo_transport(&t, "unpublished@example.org").await?;
|
||||
t.set_transport_unpublished("unpublished@example.org", true)
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS);
|
||||
t.try_make_space_for_new_relay().await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS - 1);
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
("unpublished@example.org",),
|
||||
)
|
||||
.await?,
|
||||
false
|
||||
);
|
||||
|
||||
// Test that if there are multiple unpublished relays,
|
||||
// the one that was used least recently is removed
|
||||
t.set_transport_unpublished("transport0@example.org", true)
|
||||
.await?;
|
||||
add_pseudo_transport(&t, "other_unpublished@example.org").await?;
|
||||
t.set_transport_unpublished("other_unpublished@example.org", true)
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS);
|
||||
|
||||
let transport0_id: u32 = t
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM transports WHERE addr=?",
|
||||
("transport0@example.org",),
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let other_unpublished_id: u32 = t
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM transports WHERE addr=?",
|
||||
("other_unpublished@example.org",),
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
update_transport_last_rcvd_timestamp(&t, transport0_id).await?;
|
||||
SystemTime::shift(std::time::Duration::from_secs(10));
|
||||
update_transport_last_rcvd_timestamp(&t, other_unpublished_id).await?;
|
||||
|
||||
// Test that try_make_place_for_new_relay()
|
||||
// removes the relay with the oldest last_rcvd_timestamp
|
||||
t.try_make_space_for_new_relay().await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS - 1);
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
("transport0@example.org",),
|
||||
)
|
||||
.await?,
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
("other_unpublished@example.org",),
|
||||
)
|
||||
.await?,
|
||||
true
|
||||
);
|
||||
|
||||
// Test that try_make_place_for_new_relay() fails
|
||||
// if there are MAX_RELAYS published transports
|
||||
add_pseudo_transport(&t, "published_extra@example.org").await?;
|
||||
t.set_transport_unpublished("other_unpublished@example.org", false)
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS);
|
||||
assert!(t.try_make_space_for_new_relay().await.is_err());
|
||||
assert_eq!(t.count_transports().await?, MAX_RELAYS);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Actually downloads a message partially downloaded before if the message is available on the
|
||||
/// session transport, in which case returns `Some`. If the message is available on another
|
||||
/// session transport, in which case returns `Some`. If the message is not available on this
|
||||
/// transport, returns `None`.
|
||||
///
|
||||
/// Most messages are downloaded automatically on fetch instead.
|
||||
@@ -145,29 +145,24 @@ pub(crate) async fn download_msg(
|
||||
let row = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder, transport_id FROM imap
|
||||
"SELECT uid, folder FROM imap
|
||||
WHERE rfc724_mid=? AND target!=''
|
||||
ORDER BY transport_id=? DESC LIMIT 1",
|
||||
AND transport_id=?
|
||||
LIMIT 1",
|
||||
(&rfc724_mid, transport_id),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
let msg_transport_id: u32 = row.get(2)?;
|
||||
Ok((server_uid, server_folder, msg_transport_id))
|
||||
Ok((server_uid, server_folder))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some((server_uid, server_folder, msg_transport_id)) = row else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
let Some((server_uid, server_folder)) = row else {
|
||||
// No IMAP record found, the message is not available on this transport.
|
||||
delete_from_available_post_msgs(context, &rfc724_mid).await?;
|
||||
return Err(anyhow!(
|
||||
"IMAP location for {rfc724_mid:?} post-message is unknown"
|
||||
));
|
||||
};
|
||||
if msg_transport_id != transport_id {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
Box::pin(session.fetch_single_msg(context, &server_folder, server_uid, rfc724_mid)).await?;
|
||||
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
@@ -283,35 +278,39 @@ pub(crate) async fn download_msgs(context: &Context, session: &mut Session) -> R
|
||||
.await?;
|
||||
|
||||
for rfc724_mid in &rfc724_mids {
|
||||
let res = download_msg(context, rfc724_mid.clone(), session).await;
|
||||
if let Ok(Some(())) = res {
|
||||
delete_from_downloads(context, rfc724_mid).await?;
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
}
|
||||
if let Err(err) = res {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to download message rfc724_mid={rfc724_mid}: {:#}.", err
|
||||
);
|
||||
if !msg_is_downloaded_for(context, rfc724_mid).await? {
|
||||
// This is probably a classical email that vanished before we could download it
|
||||
warn!(
|
||||
context,
|
||||
"{rfc724_mid} download failed and there is no downloaded pre-message."
|
||||
);
|
||||
delete_from_downloads(context, rfc724_mid).await?;
|
||||
} else if available_post_msgs_contains_rfc724_mid(context, rfc724_mid).await? {
|
||||
warn!(
|
||||
context,
|
||||
"{rfc724_mid} is in available_post_msgs table but we failed to fetch it,
|
||||
so set the message to DownloadState::Failure - probably it was deleted on the server in the meantime"
|
||||
);
|
||||
set_state_to_failure(context, rfc724_mid).await?;
|
||||
match download_msg(context, rfc724_mid.clone(), session).await {
|
||||
Ok(Some(())) => {
|
||||
delete_from_downloads(context, rfc724_mid).await?;
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
} else {
|
||||
// leave the message in DownloadState::InProgress;
|
||||
// it will be downloaded once it arrives.
|
||||
}
|
||||
Ok(None) => {
|
||||
// The message is not available on this transport.
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to download message rfc724_mid={rfc724_mid}: {:#}.", err
|
||||
);
|
||||
if !msg_is_downloaded_for(context, rfc724_mid).await? {
|
||||
// This is probably a classical email that vanished before we could download it
|
||||
warn!(
|
||||
context,
|
||||
"{rfc724_mid} download failed and there is no downloaded pre-message."
|
||||
);
|
||||
delete_from_downloads(context, rfc724_mid).await?;
|
||||
} else if available_post_msgs_contains_rfc724_mid(context, rfc724_mid).await? {
|
||||
warn!(
|
||||
context,
|
||||
"{rfc724_mid} is in available_post_msgs table but we failed to fetch it,
|
||||
so set the message to DownloadState::Failure - probably it was deleted on the server in the meantime"
|
||||
);
|
||||
set_state_to_failure(context, rfc724_mid).await?;
|
||||
delete_from_downloads(context, rfc724_mid).await?;
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
} else {
|
||||
// leave the message in DownloadState::InProgress;
|
||||
// it will be downloaded once it arrives.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
src/html.rs
24
src/html.rs
@@ -21,6 +21,7 @@ use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::parse_message_id;
|
||||
use crate::param::{Param::SendHtml, Params};
|
||||
use crate::plaintext::PlainText;
|
||||
use crate::simplify::unescape_message_footer_marks;
|
||||
use crate::sql;
|
||||
use crate::tools::{buf_compress, buf_decompress};
|
||||
|
||||
@@ -174,14 +175,14 @@ impl HtmlMsgParser {
|
||||
if self.html.is_empty()
|
||||
&& let Ok(decoded_data) = mail.get_body()
|
||||
{
|
||||
self.html = decoded_data;
|
||||
self.html = unescape_message_footer_marks(&decoded_data);
|
||||
}
|
||||
} else if mimetype == mime::TEXT_PLAIN
|
||||
&& self.plain.is_none()
|
||||
&& let Ok(decoded_data) = mail.get_body()
|
||||
{
|
||||
self.plain = Some(PlainText {
|
||||
text: decoded_data,
|
||||
text: unescape_message_footer_marks(&decoded_data),
|
||||
flowed: if let Some(format) = mail.ctype.params.get("format") {
|
||||
format.as_str().eq_ignore_ascii_case("flowed")
|
||||
} else {
|
||||
@@ -417,6 +418,25 @@ test some special html-characters as < > and & but also " and &#x
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_htmlparse_plain_escaped_footer() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_plain_escaped_footer.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head><body dir="auto" style="unicode-bidi: plaintext">
|
||||
-- escaped footer 1<br/>
|
||||
-- escaped footer 2<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_htmlparse_html() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
44
src/imap.rs
44
src/imap.rs
@@ -350,8 +350,8 @@ impl Imap {
|
||||
|
||||
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
|
||||
let mut first_error = None;
|
||||
for lp in login_params {
|
||||
info!(context, "IMAP trying to connect to {}.", &lp.connection);
|
||||
'candidate: for lp in login_params {
|
||||
info!(context, "IMAP trying to connect to {}.", lp.connection);
|
||||
let connection_candidate = lp.connection.clone();
|
||||
let client = match Client::connect(
|
||||
context,
|
||||
@@ -366,7 +366,7 @@ impl Imap {
|
||||
Err(err) => {
|
||||
warn!(context, "{err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
continue;
|
||||
continue 'candidate;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -387,26 +387,47 @@ impl Imap {
|
||||
user: imap_user.into(),
|
||||
access_token: token,
|
||||
};
|
||||
client.authenticate("XOAUTH2", auth).await
|
||||
client
|
||||
.authenticate("XOAUTH2", auth)
|
||||
.await
|
||||
.map(|session| (session, None))
|
||||
} else {
|
||||
info!(context, "Logging into IMAP server with LOGIN.");
|
||||
client.login(imap_user, imap_pw).await
|
||||
};
|
||||
|
||||
match login_res {
|
||||
Ok(mut session) => {
|
||||
let capabilities = determine_capabilities(&mut session).await?;
|
||||
Ok((mut session, login_capabilities_opt)) => {
|
||||
let capabilities = if let Some(login_capabilities) = login_capabilities_opt {
|
||||
login_capabilities
|
||||
} else {
|
||||
// OK response did not contain the CAPABILITY response code.
|
||||
// Request capabilities explicitly.
|
||||
match determine_capabilities(&mut session).await {
|
||||
Ok(capabilities) => capabilities,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to determine capabilities: {err:#}.");
|
||||
continue 'candidate;
|
||||
}
|
||||
}
|
||||
};
|
||||
let resync_request_sender = self.resync_request_sender.clone();
|
||||
|
||||
let session = if capabilities.can_compress {
|
||||
info!(context, "Enabling IMAP compression.");
|
||||
let compressed_session = session
|
||||
let compressed_session = match session
|
||||
.compress(|s| {
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(s);
|
||||
session_stream
|
||||
})
|
||||
.await
|
||||
.context("Failed to enable IMAP compression")?;
|
||||
{
|
||||
Ok(compressed_session) => compressed_session,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to enable IMAP compression: {err:#}.");
|
||||
continue 'candidate;
|
||||
}
|
||||
};
|
||||
Session::new(
|
||||
compressed_session,
|
||||
capabilities,
|
||||
@@ -1182,6 +1203,7 @@ impl Session {
|
||||
|
||||
for (request_uids, set) in build_sequence_sets(&request_uids)? {
|
||||
info!(context, "Starting UID FETCH of message set \"{}\".", set);
|
||||
let transport_id = self.transport_id();
|
||||
let mut fetch_responses = self
|
||||
.uid_fetch(&set, BODY_FULL)
|
||||
.await
|
||||
@@ -1277,6 +1299,12 @@ impl Session {
|
||||
"Passing message UID {} to receive_imf().", request_uid
|
||||
);
|
||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
|
||||
crate::sql::update_transport_last_rcvd_timestamp(context, transport_id)
|
||||
.await
|
||||
.context(format!(
|
||||
"Failed to update last_rcvd_timestamp of transport {}",
|
||||
transport_id
|
||||
))?;
|
||||
|
||||
// If there was an error receiving the message, show a device message:
|
||||
let received_msg = match res {
|
||||
|
||||
@@ -53,10 +53,24 @@ fn alpn(port: u16) -> &'static str {
|
||||
pub(crate) async fn determine_capabilities(
|
||||
session: &mut ImapSession<Box<dyn SessionStream>>,
|
||||
) -> Result<Capabilities> {
|
||||
let caps = session
|
||||
let imap_capabilities = session
|
||||
.capabilities()
|
||||
.await
|
||||
.context("CAPABILITY command error")?;
|
||||
identify_server(session, imap_capabilities).await
|
||||
}
|
||||
|
||||
/// Identifies the server by sending ID command if it is supported.
|
||||
///
|
||||
/// Some IMAP servers require sending this command,
|
||||
/// see <https://github.com/chatmail/core/issues/3458>,
|
||||
/// <https://github.com/OfflineIMAP/offlineimap3/issues/71>
|
||||
/// and <https://github.com/pimalaya/himalaya/issues/652>
|
||||
/// for details.
|
||||
pub(crate) async fn identify_server(
|
||||
session: &mut ImapSession<Box<dyn SessionStream>>,
|
||||
caps: async_imap::types::Capabilities,
|
||||
) -> Result<Capabilities> {
|
||||
let server_id = if caps.has_str("ID") {
|
||||
session.id([("name", Some("Delta Chat"))]).await?
|
||||
} else {
|
||||
@@ -82,18 +96,30 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs in with the LOGIN command.
|
||||
///
|
||||
/// If the server supports [ID extension], sends ID command
|
||||
/// and records the server response in the [`Capabilities`] structure.
|
||||
///
|
||||
/// [ID extension]: https://datatracker.ietf.org/doc/rfc2971/
|
||||
pub(crate) async fn login(
|
||||
self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<ImapSession<Box<dyn SessionStream>>> {
|
||||
) -> Result<(ImapSession<Box<dyn SessionStream>>, Option<Capabilities>)> {
|
||||
let Client { inner, .. } = self;
|
||||
|
||||
let session = inner
|
||||
.login(username, password)
|
||||
let (mut session, login_capabilities_opt) = inner
|
||||
.login_with_capabilities(username, password)
|
||||
.await
|
||||
.map_err(|(err, _client)| err)?;
|
||||
Ok(session)
|
||||
|
||||
let capabilities = if let Some(login_capabilities) = login_capabilities_opt {
|
||||
Some(identify_server(&mut session, login_capabilities).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((session, capabilities))
|
||||
}
|
||||
|
||||
pub(crate) async fn authenticate(
|
||||
|
||||
219
src/reaction.rs
219
src/reaction.rs
@@ -18,7 +18,7 @@ use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
@@ -199,6 +199,39 @@ async fn set_msg_id_reaction(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds or updates a pending reaction to `pending_reactions` table.
|
||||
async fn set_pending_reaction(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
reaction: &Reaction,
|
||||
) -> Result<()> {
|
||||
if reaction.is_empty() {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM pending_reactions
|
||||
WHERE rfc724_mid = ?1
|
||||
AND contact_id = ?2",
|
||||
(rfc724_mid, contact_id),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO pending_reactions (rfc724_mid, contact_id, reaction, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4)
|
||||
ON CONFLICT(rfc724_mid, contact_id)
|
||||
DO UPDATE SET reaction=excluded.reaction, timestamp=excluded.timestamp",
|
||||
(rfc724_mid, contact_id, reaction.as_str(), timestamp),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
|
||||
///
|
||||
/// `reaction` is a string consisting of a single emoji. Use
|
||||
@@ -235,6 +268,9 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
|
||||
///
|
||||
/// `reaction` is string representing the emoji. It can be empty
|
||||
/// if contact wants to remove the reaction.
|
||||
///
|
||||
/// Delegates to [`set_msg_reaction_if_present`],
|
||||
/// and if message is not present, fallbacks to [`set_pending_reaction`].
|
||||
pub(crate) async fn set_msg_reaction(
|
||||
context: &Context,
|
||||
in_reply_to: &str,
|
||||
@@ -244,8 +280,43 @@ pub(crate) async fn set_msg_reaction(
|
||||
reaction: Reaction,
|
||||
is_incoming_fresh: bool,
|
||||
) -> Result<()> {
|
||||
if !set_msg_reaction_if_present(
|
||||
context,
|
||||
in_reply_to,
|
||||
chat_id,
|
||||
contact_id,
|
||||
timestamp,
|
||||
&reaction,
|
||||
is_incoming_fresh,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Can't assign reaction to unknown message with Message-ID {}; inserting into pending table.",
|
||||
in_reply_to
|
||||
);
|
||||
let rfc724_mid = in_reply_to.trim_start_matches('<').trim_end_matches('>');
|
||||
set_pending_reaction(context, rfc724_mid, contact_id, timestamp, &reaction).await?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Similar to [`set_msg_reaction`],
|
||||
/// but does not create a row in `pending_messages` if message is not present.
|
||||
///
|
||||
/// Returns `Ok(true)` if message was present and `Ok(false)` if not.
|
||||
pub(crate) async fn set_msg_reaction_if_present(
|
||||
context: &Context,
|
||||
in_reply_to: &str,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
reaction: &Reaction,
|
||||
is_incoming_fresh: bool,
|
||||
) -> Result<bool> {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, reaction).await?;
|
||||
|
||||
if is_incoming_fresh
|
||||
&& !reaction.is_empty()
|
||||
@@ -255,15 +326,82 @@ pub(crate) async fn set_msg_reaction(
|
||||
chat_id,
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
reaction: reaction.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Applies pending reactions to message `rfc724_mid`, assuming `chat_id`.
|
||||
///
|
||||
/// Does not check if the `chat_id` is correct for this message.
|
||||
pub(crate) async fn apply_pending_reactions(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
chat_id: ChatId,
|
||||
) -> Result<()> {
|
||||
let pending_reactions: BTreeMap<ContactId, (Reaction, i64)> = context
|
||||
.sql
|
||||
.query_map_collect(
|
||||
"SELECT contact_id, reaction, timestamp
|
||||
FROM pending_reactions
|
||||
WHERE rfc724_mid=?",
|
||||
(rfc724_mid,),
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get(0)?;
|
||||
let reaction: Reaction = Reaction::new(row.get::<_, String>(1)?.as_str());
|
||||
let timestamp: i64 = row.get(2)?;
|
||||
Ok((contact_id, (reaction, timestamp)))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if pending_reactions.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Applying {} pending reactions to {}.",
|
||||
pending_reactions.len(),
|
||||
rfc724_mid
|
||||
);
|
||||
|
||||
for (contact_id, (reaction, timestamp)) in pending_reactions {
|
||||
// We know whether the reaction message is incoming,
|
||||
// but it is unclear whether it's still fresh (i.e. unnoticed by the user).
|
||||
// To be safe, always count it as fresh, to notify the user rather once too often than once too few.
|
||||
// This is only relevant in edge cases, anyway.
|
||||
let is_incoming_fresh = contact_id != ContactId::SELF;
|
||||
if !set_msg_reaction_if_present(
|
||||
context,
|
||||
rfc724_mid,
|
||||
chat_id,
|
||||
contact_id,
|
||||
timestamp,
|
||||
&reaction,
|
||||
is_incoming_fresh,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
bail!("Message {rfc724_mid} is not present, can't apply pending reactions");
|
||||
}
|
||||
}
|
||||
|
||||
// Note: race condition can't happen here,
|
||||
// as at this point the message is already added to the DB,
|
||||
// so no new pending reactions with this rfc724_mid will be added in meantime.
|
||||
// (Assuming this function is used after receiving the message.)
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM pending_reactions WHERE rfc724_mid=?",
|
||||
(rfc724_mid,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -713,6 +851,71 @@ Content-Disposition: reaction\n\
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_out_of_order_reaction() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let charlie = tcm.charlie().await;
|
||||
|
||||
tcm.send_recv_accept(&alice, &bob, "hi").await;
|
||||
tcm.send_recv_accept(&alice, &charlie, "hi").await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
let groupchat_alice = alice
|
||||
.create_group_with_members("test", &[&bob, &charlie])
|
||||
.await;
|
||||
|
||||
// one to one
|
||||
|
||||
{
|
||||
let alice_msg = alice.send_text(chat_alice.id, "test").await;
|
||||
|
||||
send_reaction(&alice, alice_msg.sender_msg_id, "👍").await?;
|
||||
let reaction_msg = alice.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg_hidden(&reaction_msg).await;
|
||||
let msg = bob.recv_msg(&alice_msg).await;
|
||||
assert_eq!(get_msg_reactions(&bob, msg.id).await?.reactions.len(), 1);
|
||||
}
|
||||
|
||||
// group
|
||||
|
||||
{
|
||||
let alice_msg = alice.send_text(groupchat_alice, "test").await;
|
||||
|
||||
send_reaction(&alice, alice_msg.sender_msg_id, "👍").await?;
|
||||
let reaction_msg_alice = alice.pop_sent_msg().await;
|
||||
|
||||
let charlie_msg = charlie.recv_msg(&alice_msg).await;
|
||||
send_reaction(&charlie, charlie_msg.id, "👍").await?;
|
||||
let reaction_msg_charlie = charlie.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg_hidden(&reaction_msg_alice).await;
|
||||
bob.recv_msg_hidden(&reaction_msg_charlie).await;
|
||||
let msg = bob.recv_msg(&alice_msg).await;
|
||||
assert_eq!(get_msg_reactions(&bob, msg.id).await?.reactions.len(), 2);
|
||||
}
|
||||
|
||||
// react and remove reaction
|
||||
|
||||
{
|
||||
let alice_msg = alice.send_text(chat_alice.id, "test").await;
|
||||
|
||||
send_reaction(&alice, alice_msg.sender_msg_id, "👍").await?;
|
||||
let reaction_msg = alice.pop_sent_msg().await;
|
||||
|
||||
send_reaction(&alice, alice_msg.sender_msg_id, "").await?;
|
||||
let remove_reaction_msg = alice.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg_hidden(&reaction_msg).await;
|
||||
bob.recv_msg_hidden(&remove_reaction_msg).await;
|
||||
let msg = bob.recv_msg(&alice_msg).await;
|
||||
assert_eq!(get_msg_reactions(&bob, msg.id).await?.reactions.len(), 0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_summary(t: &TestContext, expected: &str) {
|
||||
let chatlist = Chatlist::try_load(t, 0, None, None).await.unwrap();
|
||||
let summary = chatlist.get_summary(t, 0, None).await.unwrap();
|
||||
|
||||
@@ -724,7 +724,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
is_old_contact_request = chat_id_blocked == Blocked::Request && !is_created;
|
||||
|
||||
// Add parts
|
||||
add_parts(
|
||||
let msg = add_parts(
|
||||
context,
|
||||
&mut mime_parser,
|
||||
imf_raw,
|
||||
@@ -740,7 +740,13 @@ pub(crate) async fn receive_imf_inner(
|
||||
is_created,
|
||||
)
|
||||
.await
|
||||
.context("add_parts error")?
|
||||
.context("add_parts error")?;
|
||||
|
||||
crate::reaction::apply_pending_reactions(context, rfc724_mid, chat_id)
|
||||
.await
|
||||
.context("Failed to apply pending reactions")?;
|
||||
|
||||
msg
|
||||
};
|
||||
|
||||
if !from_id.is_special() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! # Simplify incoming plaintext.
|
||||
use crate::tools::IsNoneOrEmpty;
|
||||
|
||||
/// Protects lines starting with `--` against being treated as a footer.
|
||||
/// Protects lines starting with `-- ` against being treated as a footer.
|
||||
/// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||
/// this should be invisible on most systems and there is no need to unescape it again
|
||||
/// (which won't be done by non-deltas anyway).
|
||||
@@ -10,10 +10,27 @@ use crate::tools::IsNoneOrEmpty;
|
||||
/// but for non-delta-compatibility, that seems to be better.
|
||||
/// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
if let Some(text) = text.strip_prefix("--") {
|
||||
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
|
||||
if let Some(text) = text.strip_prefix("-- ") {
|
||||
"-\u{200B}- ".to_string() + &text.replace("\n-- ", "\n-\u{200B}- ")
|
||||
} else {
|
||||
text.replace("\n--", "\n-\u{200B}-")
|
||||
text.replace("\n-- ", "\n-\u{200B}- ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Unapplies changes made by [`escape_message_footer_marks`].
|
||||
///
|
||||
/// This assumes original message doesn't contain `LF DASH ZWSP DASH` (`\n-\u{200B}-`)
|
||||
/// character sequence and doesn't start with `DASH ZWSP DASH` (`-\u{200B}-`)
|
||||
/// character sequence.
|
||||
/// Such sequences are unlikely to appear in normal text communication.
|
||||
///
|
||||
/// For backward compatibility, un-escaping as opposed to escaping
|
||||
/// doesn't require the final space to be present.
|
||||
pub fn unescape_message_footer_marks(text: &str) -> String {
|
||||
if let Some(text) = text.strip_prefix("-\u{200B}-") {
|
||||
"--".to_string() + &text.replace("\n-\u{200B}-", "\n--")
|
||||
} else {
|
||||
text.replace("\n-\u{200B}-", "\n--")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,8 +306,7 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
|
||||
if is_cut_at_end && !ret.is_empty() {
|
||||
ret += " [...]";
|
||||
}
|
||||
// redo escaping done by escape_message_footer_marks()
|
||||
ret.replace('\u{200B}', "")
|
||||
unescape_message_footer_marks(&ret)
|
||||
}
|
||||
|
||||
/// Returns true if the line contains only whitespace.
|
||||
@@ -467,12 +483,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_message_footer_marks() {
|
||||
let esc = escape_message_footer_marks("--\n--text --in line");
|
||||
assert_eq!(esc, "-\u{200B}-\n-\u{200B}-text --in line");
|
||||
fn test_escape_unescape_message_footer_marks() {
|
||||
let original = "-- \n-- text -- in line";
|
||||
let esc = escape_message_footer_marks(original);
|
||||
assert_eq!(esc, "-\u{200B}- \n-\u{200B}- text -- in line");
|
||||
let unesc = unescape_message_footer_marks(&esc);
|
||||
assert_eq!(unesc, original);
|
||||
}
|
||||
|
||||
let esc = escape_message_footer_marks("--\r\n--text");
|
||||
assert_eq!(esc, "-\u{200B}-\r\n-\u{200B}-text");
|
||||
#[test]
|
||||
fn test_escape_unescape_message_footer_marks_crlf() {
|
||||
let original = "-- \r\n-- text";
|
||||
let esc = escape_message_footer_marks(original);
|
||||
assert_eq!(esc, "-\u{200B}- \r\n-\u{200B}- text");
|
||||
let unesc = unescape_message_footer_marks(&esc);
|
||||
assert_eq!(unesc, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -515,7 +540,7 @@ mod tests {
|
||||
assert!(!is_cut);
|
||||
assert_eq!(footer.unwrap(), "footer");
|
||||
|
||||
let input = "text\n\n--\ntreated as footer when unescaped".to_string();
|
||||
let input = "text\n\n-- \ntreated as footer when unescaped".to_string();
|
||||
let SimplifiedText {
|
||||
text,
|
||||
is_cut,
|
||||
@@ -532,7 +557,7 @@ mod tests {
|
||||
footer,
|
||||
..
|
||||
} = simplify(escaped, true);
|
||||
assert_eq!(text, "text\n\n--\ntreated as footer when unescaped");
|
||||
assert_eq!(text, "text\n\n-- \ntreated as footer when unescaped");
|
||||
assert!(!is_cut);
|
||||
assert_eq!(footer, None);
|
||||
|
||||
@@ -557,7 +582,7 @@ mod tests {
|
||||
assert!(!is_cut);
|
||||
assert_eq!(footer, None);
|
||||
|
||||
let input = "--\ntreated as footer when unescaped".to_string();
|
||||
let input = "-- \ntreated as footer when unescaped".to_string();
|
||||
let SimplifiedText {
|
||||
text,
|
||||
is_cut,
|
||||
@@ -575,7 +600,7 @@ mod tests {
|
||||
footer,
|
||||
..
|
||||
} = simplify(escaped, true);
|
||||
assert_eq!(text, "--\ntreated as footer when unescaped");
|
||||
assert_eq!(text, "-- \ntreated as footer when unescaped");
|
||||
assert!(!is_cut);
|
||||
assert_eq!(footer, None);
|
||||
}
|
||||
|
||||
64
src/sql.rs
64
src/sql.rs
@@ -47,6 +47,9 @@ mod pool;
|
||||
|
||||
use pool::{Pool, WalCheckpointStats};
|
||||
|
||||
/// How long a hidden and unused transport should be kept in the database before being deleted.
|
||||
pub const UNPUBLISHED_TRANSPORT_KEEP_TIME: i64 = 90 * 24 * 60 * 60;
|
||||
|
||||
/// A wrapper around the underlying Sqlite3 object.
|
||||
#[derive(Debug)]
|
||||
pub struct Sql {
|
||||
@@ -908,10 +911,71 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
remove_unused_hidden_transports(context)
|
||||
.await
|
||||
.context("Failed to remove unused hidden transports")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
remove_old_pending_reactions(context)
|
||||
.await
|
||||
.context("Failed to remove old pending reactions")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
info!(context, "Housekeeping done.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes pending reactions that weren't applied for 90 days.
|
||||
async fn remove_old_pending_reactions(context: &Context) -> Result<usize> {
|
||||
let three_months_ago = time().saturating_sub(60 * 60 * 24 * 90);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM pending_reactions WHERE timestamp < ?",
|
||||
(three_months_ago,),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Removes transports that are hidden (`is_published=0`),
|
||||
/// and haven't been used to receive new messages for [`UNPUBLISHED_TRANSPORT_KEEP_TIME`] seconds.
|
||||
pub(crate) async fn remove_unused_hidden_transports(context: &Context) -> Result<usize> {
|
||||
let now = time();
|
||||
let cutoff = now.saturating_sub(UNPUBLISHED_TRANSPORT_KEEP_TIME);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM transports
|
||||
WHERE is_published=0
|
||||
AND last_rcvd_timestamp<?1
|
||||
AND add_timestamp<?1", // important, prevents immediate deletion in case of `last_rcvd_timestamp=0`
|
||||
(cutoff,),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Updates transport's `last_rcvd_timestamp`
|
||||
/// with the current time.
|
||||
///
|
||||
/// This is used to postpone deletion of hidden transport by [`remove_unused_hidden_transports`],
|
||||
/// if it is still used to receive messages.
|
||||
pub(crate) async fn update_transport_last_rcvd_timestamp(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
) -> Result<usize> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE transports
|
||||
SET last_rcvd_timestamp=?1
|
||||
WHERE id=?2",
|
||||
(time(), transport_id),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get the value of a column `idx` of the `row` as `Vec<u8>`.
|
||||
pub fn row_get_vec(row: &Row, idx: usize) -> rusqlite::Result<Vec<u8>> {
|
||||
row.get(idx).or_else(|err| match row.get_ref(idx)? {
|
||||
|
||||
@@ -2456,6 +2456,72 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed.
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 155)?;
|
||||
if dbversion < migration_version {
|
||||
// last_rcvd_timestamp tracks timestamp of the last received message
|
||||
// on the transport. This is used to automatically remove hidden transports
|
||||
// that were not used to receive messages for some time.
|
||||
//
|
||||
// NOTE: ideally we would use `DEFAULT (unixepoch())`,
|
||||
// but sqlite forbids non-constant default in ALTER TABLE.
|
||||
// Instead, we need to remember to also check `add_timestamp`
|
||||
// before removing transport
|
||||
// (so it won't be immediately deleted if last_rcvd_timestamp=0).
|
||||
sql.execute_migration(
|
||||
"
|
||||
ALTER TABLE transports
|
||||
ADD last_rcvd_timestamp INTEGER NOT NULL DEFAULT 0
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 156)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"DROP INDEX IF EXISTS msgs_index7;
|
||||
CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id, timestamp);",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 157)?;
|
||||
if dbversion < migration_version {
|
||||
// Ensure that existing transports
|
||||
// don't start out with last_rcvd_timestamp=0,
|
||||
// so that they are not immediately deleted
|
||||
sql.execute_migration_transaction(
|
||||
|transaction| {
|
||||
transaction.execute(
|
||||
"UPDATE transports SET last_rcvd_timestamp=?1 WHERE last_rcvd_timestamp=0",
|
||||
(tools::time(),),
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 158)?;
|
||||
if dbversion < migration_version {
|
||||
// Stores reactions to not-yet-received messages.
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE pending_reactions (
|
||||
rfc724_mid TEXT NOT NULL,
|
||||
contact_id INTEGER NOT NULL,
|
||||
reaction TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
PRIMARY KEY(rfc724_mid, contact_id),
|
||||
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE
|
||||
) STRICT",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::message::Message;
|
||||
use crate::tools::SystemTime;
|
||||
use crate::{EventType, test_utils::TestContext};
|
||||
|
||||
#[test]
|
||||
@@ -367,3 +368,114 @@ async fn test_incremental_vacuum() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_remove_unused_hidden_transports() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let now = time();
|
||||
|
||||
let transport_id = t
|
||||
.sql
|
||||
.transaction(|t| {
|
||||
// published transport
|
||||
t.execute(
|
||||
"INSERT INTO transports
|
||||
(addr,
|
||||
entered_param,
|
||||
configured_param,
|
||||
add_timestamp)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
("1", "", "", now),
|
||||
)?;
|
||||
|
||||
// not published transport
|
||||
let transport_id = t.query_row::<u32, _, _>(
|
||||
"INSERT INTO transports
|
||||
(addr,
|
||||
entered_param,
|
||||
configured_param,
|
||||
is_published,
|
||||
add_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id",
|
||||
("2", "", "", 0, now),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
// this transport has add_timestamp in the future,
|
||||
// and should not be removed at all, since we only shift time
|
||||
// by 90 days and one second.
|
||||
t.execute(
|
||||
"INSERT INTO transports
|
||||
(addr,
|
||||
entered_param,
|
||||
configured_param,
|
||||
is_published,
|
||||
add_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
("3", "", "", 0, now + 60),
|
||||
)?;
|
||||
Ok(transport_id)
|
||||
})
|
||||
.await?;
|
||||
|
||||
async fn assert_transport_removed(
|
||||
context: &Context,
|
||||
hidden_transport_id: u32,
|
||||
should_be_removed: bool,
|
||||
) -> Result<()> {
|
||||
let transports_count: u32 = context
|
||||
.sql
|
||||
.query_get_value("SELECT count(*) FROM transports", ())
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(transports_count, if should_be_removed { 2 } else { 3 });
|
||||
let hidden_transport_present: bool = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT count(*) FROM transports WHERE id=?",
|
||||
(hidden_transport_id,),
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(hidden_transport_present, !should_be_removed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.query_get_value::<i64>(
|
||||
"SELECT last_rcvd_timestamp FROM transports WHERE id=?",
|
||||
(transport_id,)
|
||||
)
|
||||
.await?
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
update_transport_last_rcvd_timestamp(&t, transport_id).await?;
|
||||
// last_rcvd_timestamp should update
|
||||
assert!(
|
||||
t.sql
|
||||
.query_get_value::<i64>(
|
||||
"SELECT last_rcvd_timestamp FROM transports WHERE id=?",
|
||||
(transport_id,)
|
||||
)
|
||||
.await?
|
||||
.unwrap()
|
||||
>= now
|
||||
);
|
||||
|
||||
assert_transport_removed(&t, transport_id, false).await?;
|
||||
remove_unused_hidden_transports(&t).await?;
|
||||
// it was recently used, so nothing is deleted
|
||||
assert_transport_removed(&t, transport_id, false).await?;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(
|
||||
(UNPUBLISHED_TRANSPORT_KEEP_TIME + 1).try_into()?,
|
||||
));
|
||||
|
||||
remove_unused_hidden_transports(&t).await?;
|
||||
assert_transport_removed(&t, transport_id, true).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
8
test-data/message/text_plain_escaped_footer.eml
Normal file
8
test-data/message/text_plain_escaped_footer.eml
Normal file
@@ -0,0 +1,8 @@
|
||||
Message-Id: <lkjsdf01u@example.org>
|
||||
Date: Sat, 14 Sep 2019 19:00:13 +0200
|
||||
From: lmn <x@tux.org>
|
||||
To: abc <abc@bcd.com>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
-- escaped footer 1
|
||||
-- escaped footer 2
|
||||
Reference in New Issue
Block a user