Compare commits

..

1 Commits

Author SHA1 Message Date
Hocuri
61fb9e7e3b test: Revert Mark receive_imf() as only for tests and "internals" feature
Unfortunately, this made compiling the `receive_emails` benchmark fail because it depends on `receive_imf()`, and I don't think there is a nice way to enable this function for the benchmark.
2024-11-28 15:44:33 +01:00
106 changed files with 2620 additions and 4783 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.84.0
RUSTUP_TOOLCHAIN: 1.82.0
steps:
- uses: actions/checkout@v4
with:
@@ -37,10 +37,8 @@ jobs:
run: cargo fmt --all -- --check
- name: Run clippy
run: scripts/clippy.sh
- name: Check with all features
- name: Check
run: cargo check --workspace --all-targets --all-features
- name: Check with only default features
run: cargo check --all-targets
npm_constants:
name: Check if node constants are up to date
@@ -97,11 +95,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.84.0
rust: 1.82.0
- os: windows-latest
rust: 1.84.0
rust: 1.82.0
- os: macos-latest
rust: 1.84.0
rust: 1.82.0
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest
@@ -152,7 +150,7 @@ jobs:
uses: swatinem/rust-cache@v2
- name: Build C library
run: cargo build -p deltachat_ffi
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Upload C library
uses: actions/upload-artifact@v4
@@ -223,11 +221,11 @@ jobs:
- os: macos-latest
python: pypy3.10
# Minimum Supported Python Version = 3.8
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built. Test it with minimum supported Rust version.
- os: ubuntu-latest
python: 3.8
python: 3.7
runs-on: ${{ matrix.os }}
steps:
@@ -277,9 +275,9 @@ jobs:
- os: macos-latest
python: pypy3.10
# Minimum Supported Python Version = 3.8
# Minimum Supported Python Version = 3.7
- os: ubuntu-latest
python: 3.8
python: 3.7
runs-on: ${{ matrix.os }}
steps:

View File

@@ -1,254 +1,5 @@
# Changelog
## [1.154.1] - 2025-01-15
### Tests
- Expect trashing of no-op "member added" in non_member_cannot_modify_member_list.
## [1.154.0] - 2025-01-15
### Features / Changes
- New group consistency algorithm.
### Fixes
- Migration: Set bcc_self=1 if it's unset and delete_server_after!=1 ([#6432](https://github.com/deltachat/deltachat-core-rust/pull/6432)).
- Clear the config cache after every migration ([#6438](https://github.com/deltachat/deltachat-core-rust/pull/6438)).
### Build system
- Increase minimum supported Python version to 3.8.
- [**breaking**] Remove jsonrpc feature flag.
### CI
- Update Rust to 1.84.0.
### Miscellaneous Tasks
- Beta Clippy suggestions ([#6422](https://github.com/deltachat/deltachat-core-rust/pull/6422)).
### Refactor
- Use let..else.
- Add why_cant_send_ex() capable to only ignore specified conditions.
- Remove unnecessary is_contact_in_chat check.
- Eliminate remaining repeat_vars() calls ([#6359](https://github.com/deltachat/deltachat-core-rust/pull/6359)).
### Tests
- Use assert_eq! to compare chatlist length.
## [1.153.0] - 2025-01-05
### Features / Changes
- Remove "jobs" from imap_markseen if folder doesn't exist ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
- Delete `vg-request-with-auth` from IMAP after processing ([#6208](https://github.com/deltachat/deltachat-core-rust/pull/6208)).
### API-Changes
- Add `IncomingWebxdcNotify.chat_id` ([#6356](https://github.com/deltachat/deltachat-core-rust/pull/6356)).
- rpc-client: Add INCOMING_REACTION to const.EventType ([#6349](https://github.com/deltachat/deltachat-core-rust/pull/6349)).
### Documentation
- Viewtype::Sticker may be changed to Image and how to disable that ([#6352](https://github.com/deltachat/deltachat-core-rust/pull/6352)).
### Fixes
- Never change Viewtype::Sticker to Image if file has non-image extension ([#6352](https://github.com/deltachat/deltachat-core-rust/pull/6352)).
- Change BccSelf default to 0 for chatmail ([#6340](https://github.com/deltachat/deltachat-core-rust/pull/6340)).
- Mark holiday notice messages as bot-generated.
- Don't treat location-only and sync messages as bot ones ([#6357](https://github.com/deltachat/deltachat-core-rust/pull/6357)).
- Update shadowsocks crate to 1.22.0 to avoid panic when parsing some QR codes.
- Prefer to encrypt if E2eeEnabled even if peers have EncryptPreference::NoPreference.
- Prioritize mailing list over self-sent messages.
- Allow empty `To` field for self-sent messages.
- Default `to_id` to self instead of 0.
### Refactor
- Remove unused parameter and return value from `build_body_file(…)` ([#6369](https://github.com/deltachat/deltachat-core-rust/pull/6369)).
- Deprecate Param::ErroneousE2ee.
- Add `emit_msgs_changed_without_msg_id`.
- Add_parts: Remove excessive `is_mdn` checks.
- Simplify `self_sent` condition.
- Don't ignore get_for_contact errors.
### Tests
- Messages without recipients are assigned to self chat.
- Message with empty To: field should have a valid to_id.
- Fix `test_logged_ac_process_ffi_failure` flakiness.
## [1.152.2] - 2024-12-24
### Features / Changes
- Emit ImexProgress(1) after receiving backup size.
- `delete_msgs`: Use `transaction()` instead of `call_write()`.
- Start ephemeral timers when the chat is noticed.
- Start ephemeral timers when the chat is archived.
- Revalidate HTTP cache entries once per minute maximum.
### Fixes
- Reduce number of `repeat_vars()` calls.
- `sanitise_name`: Don't consider punctuation and control chars as part of file extension ([#6362](https://github.com/deltachat/deltachat-core-rust/pull/6362)).
### Refactor
- Remove marknoticed_chat_if_older_than().
### Miscellaneous Tasks
- Remove contrib/ directory.
## [1.152.1] - 2024-12-17
### Build system
- Downgrade Rust version used to build binaries.
- Reduce MSRV to 1.77.0.
## [1.152.0] - 2024-12-12
### API-Changes
- [**breaking**] Remove `dc_prepare_msg` and `dc_msg_is_increation`.
### Build system
- Increase MSRV to 1.81.0.
### Features / Changes
- Cache HTTP GET requests.
- Prefix server-url in info.
- Set `mime_modified` for the last message part, not the first ([#4462](https://github.com/deltachat/deltachat-core-rust/pull/4462)).
### Fixes
- Render "message" parts in multipart messages' HTML ([#4462](https://github.com/deltachat/deltachat-core-rust/pull/4462)).
- Ignore garbage at the end of the keys.
## [1.151.6] - 2024-12-11
### Features / Changes
- Don't add "Failed to send message to ..." info messages to group chats.
- Add info messages about implicit membership changes if group member list is recreated ([#6314](https://github.com/deltachat/deltachat-core-rust/pull/6314)).
### Fixes
- Add self-addition message to chat when recreating member list.
- Do not subscribe to heartbeat if already subscribed via metadata.
### Build system
- Add idna 0.5.0 exception into deny.toml.
### Documentation
- Update links to Node.js bindings in the README.
### Refactor
- Factor out `wait_for_all_work_done()`.
### Tests
- Notifiy more prominently & in more tests about false positives when running `cargo test` ([#6308](https://github.com/deltachat/deltachat-core-rust/pull/6308)).
## [1.151.5] - 2024-12-05
### API-Changes
- [**breaking**] Remove dc_all_work_done().
### Security
- cargo: Update rPGP to 0.14.2.
This fixes [Panics on Malformed Untrusted Input](https://github.com/rpgp/rpgp/security/advisories/GHSA-9rmp-2568-59rv)
and [Potential Resource Exhaustion when handling Untrusted Messages](https://github.com/rpgp/rpgp/security/advisories/GHSA-4grw-m28r-q285).
This allows the attacker to crash the application via specially crafted messages and keys.
We recommend all users and bot operators to upgrade to the latest version.
There is no impact on the confidentiality of the messages and keys so no action other than upgrading is needed.
### Fixes
- Store plaintext in mime_headers of truncated sent messages ([#6273](https://github.com/deltachat/deltachat-core-rust/pull/6273)).
### Documentation
- Document `push` module.
- Remove mention of non-existent `nightly` feature.
### Tests
- Fix panic in `receive_emails` benchmark ([#6306](https://github.com/deltachat/deltachat-core-rust/pull/6306)).
## [1.151.4] - 2024-12-03
### Features / Changes
- Encrypt notification tokens.
### Fixes
- Replace connectivity state "Connected" with "Preparing".
### Miscellaneous Tasks
- Beta clippy suggestions ([#6271](https://github.com/deltachat/deltachat-core-rust/pull/6271)).
### Tests
- Fix `cargo check` for `receive_emails` benchmark.
### CI
- Also run cargo check without all-features.
## [1.151.3] - 2024-12-02
### API-Changes
- Remove experimental `request_internet_access` option from webxdc's `manifest.toml`.
- Add getWebxdcHref to json api ([#6281](https://github.com/deltachat/deltachat-core-rust/pull/6281)).
### CI
- Update Rust to 1.83.0.
### Documentation
- Update dc_msg_get_info_type() and dc_get_securejoin_qr() ([#6269](https://github.com/deltachat/deltachat-core-rust/pull/6269)).
- Fix references to iroh-related headers in peer_channels docs.
- Improve CFFI docs, link to corresponding JSON-RPC docs.
### Features / Changes
- Allow the user to replace maps integration ([#5678](https://github.com/deltachat/deltachat-core-rust/pull/5678)).
- Mark saved messages chat as protected.
### Fixes
- Close iroh endpoint when I/O is stopped.
- Do not add protection messages to Saved Messages chat.
- Mark Saved Messages chat as protected if it exists.
- Sync chat action even if sync message arrives before first one from contact ([#6259](https://github.com/deltachat/deltachat-core-rust/pull/6259)).
### Refactor
- Remove some .unwrap() calls.
- Create_status_update_record: Remove double check of info_msg_id.
- Use Option::or_else() to dedup emitting IncomingWebxdcNotify.
## [1.151.2] - 2024-11-26
### API-Changes
@@ -621,7 +372,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
- Reset quota on configured address change ([#5908](https://github.com/deltachat/deltachat-core-rust/pull/5908)).
- Do not emit progress 1000 when configuration is cancelled.
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/deltachat/deltachat-core-rust/pull/5338)).
- Re-add tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
- Readd tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
### Miscellaneous Tasks
@@ -1212,7 +963,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
### Tests
- deltachat-rpc-client: re-enable `log_cli`.
- deltachat-rpc-client: reenable `log_cli`.
## [1.140.0] - 2024-06-04
@@ -2149,7 +1900,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
- Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol.
- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat.
- deltachat-repl: Enable INFO logging by default and add timestamps.
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elements based on the configuration key which is a part of the event.
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elments based on the configuration key which is a part of the event.
- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)).
- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)).
- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)).
@@ -5618,13 +5369,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.151.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.150.0..v1.151.0
[1.151.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.0..v1.151.1
[1.151.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.1..v1.151.2
[1.151.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.2..v1.151.3
[1.151.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.3..v1.151.4
[1.151.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.4..v1.151.5
[1.151.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.5..v1.151.6
[1.152.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.6..v1.152.0
[1.152.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.0..v1.152.1
[1.152.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.1..v1.152.2
[1.153.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.2..v1.153.0
[1.154.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.153.0..v1.154.0
[1.154.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.0..v1.154.1

View File

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

View File

@@ -81,7 +81,7 @@ If you want to contribute a code, follow this guide.
CI runs the tests and checks code formatting.
While it is running, self-review your PR to make sure all the changes you expect are there
and there are no accidentally committed unrelated changes and files.
and there are no accidentally commited unrelated changes and files.
Push the necessary fixup commits or force-push to your branch if needed.

711
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.154.1"
version = "1.151.2"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -39,11 +39,11 @@ format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-broadcast = "0.7.1"
async-channel = { workspace = true }
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
brotli = { version = "7", default-features=false, features = ["std"] }
@@ -52,7 +52,7 @@ chrono = { workspace = true, features = ["alloc", "clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.10"
fast-socks5 = "0.9"
fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
@@ -62,10 +62,10 @@ http-body-util = "0.1.2"
humansize = "2"
hyper = "1"
hyper-util = "0.1.10"
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.25.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
iroh-net = { version = "0.28.1", default-features = false }
kamadak-exif = "0.6.1"
kamadak-exif = "0.6.0"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
mailparse = "0.15"
@@ -76,7 +76,7 @@ num-traits = { workspace = true }
once_cell = { workspace = true }
parking_lot = "0.12"
percent-encoding = "2.3"
pgp = { version = "0.14.2", default-features = false }
pgp = { version = "0.14.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.37"
@@ -85,15 +85,15 @@ rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.10.1"
rustls = { version = "0.23.20", default-features = false }
rustls-pki-types = "1.10.0"
rustls = { version = "0.23.14", default-features = false }
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
shadowsocks = { version = "1.21.0", default-features = false, features = ["aead-cipher-2022"] }
smallvec = "1.13.2"
strum = "0.26"
strum_macros = "0.26"
@@ -101,15 +101,15 @@ tagger = "4.3.4"
textwrap = "0.16.1"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.0"
tokio-rustls = { version = "0.26.1", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
tokio-rustls = { version = "0.26.0", default-features = false }
tokio-stream = { version = "0.1.16", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.7"
webpki-roots = "0.26.6"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
@@ -120,7 +120,7 @@ nu-ansi-term = { workspace = true }
pretty_assertions = "1.4.1"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = { workspace = true }
testdir = "0.9.3"
testdir = "0.9.0"
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[workspace]
@@ -150,7 +150,6 @@ harness = false
[[bench]]
name = "receive_emails"
required-features = ["internals"]
harness = false
[[bench]]
@@ -169,12 +168,12 @@ harness = false
anyhow = "1"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.39", default-features = false }
chrono = { version = "0.4.38", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
futures = "0.3.31"
futures-lite = "2.5.0"
futures-lite = "2.4.0"
libc = "0.2"
log = "0.4"
nu-ansi-term = "0.46"
@@ -186,10 +185,10 @@ rusqlite = "0.32"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.14.0"
tempfile = "3.13.0"
thiserror = "1"
tokio = "1"
tokio-util = "0.7.13"
tokio-util = "0.7.11"
tracing-subscriber = "0.3"
yerpc = "0.6.2"

View File

@@ -161,6 +161,7 @@ $ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
## Features
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features.
## Update Provider Data
@@ -177,8 +178,8 @@ Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js**
- over JSON-RPC: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
- over CFFI[^1]: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat/)\]
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go**
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]

View File

@@ -12,18 +12,18 @@ use deltachat::{
};
use tempfile::tempdir;
async fn recv_all_emails(context: Context, iteration: u32) -> Context {
async fn recv_all_emails(context: Context) -> Context {
for i in 0..100 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Mr.{iteration}.{i}@testrun.org
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
In-Reply-To: Mr.{iteration}.{i_dec}@testrun.org
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
@@ -41,11 +41,11 @@ Hello {i}",
/// Receive 100 emails that remove charlie@example.com and add
/// him back
async fn recv_groupmembership_emails(context: Context, iteration: u32) -> Context {
async fn recv_groupmembership_emails(context: Context) -> Context {
for i in 0..50 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Gr.{iteration}.ADD.{i}@testrun.org
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
From: sender@testrun.org
@@ -53,12 +53,13 @@ Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
Chat-Group-Member-Added: charlie@example.com
In-Reply-To: Gr.{iteration}.REMOVE.{i_dec}@testrun.org
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
@@ -67,7 +68,7 @@ Hello {i}",
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Gr.{iteration}.REMOVE.{i}@testrun.org
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
From: sender@testrun.org
@@ -75,12 +76,14 @@ Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
Chat-Group-Member-Removed: charlie@example.com
In-Reply-To: Gr.{iteration}.ADD.{i}@testrun.org
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}"
Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
@@ -126,13 +129,11 @@ fn criterion_benchmark(c: &mut Criterion) {
group.bench_function("Receive 100 simple text msgs", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(create_context());
let mut i = 0;
b.to_async(&rt).iter(|| {
let ctx = context.clone();
i += 1;
async move {
recv_all_emails(black_box(ctx), i).await;
recv_all_emails(black_box(ctx)).await;
}
});
});
@@ -141,13 +142,11 @@ fn criterion_benchmark(c: &mut Criterion) {
|b| {
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(create_context());
let mut i = 0;
b.to_async(&rt).iter(|| {
let ctx = context.clone();
i += 1;
async move {
recv_groupmembership_emails(black_box(ctx), i).await;
recv_groupmembership_emails(black_box(ctx)).await;
}
});
},

80
contrib/proxy.py Normal file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
# Examples:
#
# Original server that doesn't use SSL:
# ./proxy.py 8080 imap.nauta.cu 143
# ./proxy.py 8081 smtp.nauta.cu 25
#
# Original server that uses SSL:
# ./proxy.py 8080 testrun.org 993 --ssl
# ./proxy.py 8081 testrun.org 465 --ssl
from datetime import datetime
import argparse
import selectors
import ssl
import socket
import socketserver
class Proxy(socketserver.ThreadingTCPServer):
allow_reuse_address = True
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
self.real_host = real_host
self.real_port = real_port
self.use_ssl = use_ssl
super().__init__((proxy_host, proxy_port), RequestHandler)
class RequestHandler(socketserver.BaseRequestHandler):
def handle(self):
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
total = 0
real_server = (self.server.real_host, self.server.real_port)
with socket.create_connection(real_server) as sock:
if self.server.use_ssl:
context = ssl.create_default_context()
sock = context.wrap_socket(
sock, server_hostname=real_server[0])
forward = {self.request: sock, sock: self.request}
sel = selectors.DefaultSelector()
sel.register(self.request, selectors.EVENT_READ,
self.client_address)
sel.register(sock, selectors.EVENT_READ, real_server)
active = True
while active:
events = sel.select()
for key, mask in events:
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
data = key.fileobj.recv(1024)
received = len(data)
total += received
print(data)
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
if data:
forward[key.fileobj].sendall(data)
else:
print('\nCLOSING CONNECTION.\n\n')
forward[key.fileobj].close()
key.fileobj.close()
active = False
if __name__ == '__main__':
p = argparse.ArgumentParser(description='Simple Python Proxy')
p.add_argument(
"proxy_port", help="the port where the proxy will listen", type=int)
p.add_argument('host', help="the real host")
p.add_argument('port', help="the port of the real host", type=int)
p.add_argument("--ssl", help="use ssl to connect to the real host",
action="store_true")
args = p.parse_args()
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
proxy.serve_forever()

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.154.1"
version = "1.151.2"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -15,7 +15,7 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { workspace = true, default-features = false }
deltachat-jsonrpc = { workspace = true }
deltachat-jsonrpc = { workspace = true, optional = true }
libc = { workspace = true }
human-panic = { version = "2", default-features = false }
num-traits = { workspace = true }
@@ -30,4 +30,5 @@ yerpc = { workspace = true, features = ["anyhow_expose"] }
[features]
default = ["vendored"]
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
jsonrpc = ["dep:deltachat-jsonrpc"]

View File

@@ -418,7 +418,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts
* default=send and request read receipts, only send but not request if `bot` is set
* default=send and request read receipts, only send but not reuqest if `bot` is set
* - `bcc_self` = 0=do not send a copy of outgoing messages to self,
* 1=send a copy of outgoing messages to self (default).
* Sending messages to self is needed for a proper multi-account setup,
@@ -722,6 +722,12 @@ char* dc_get_connectivity_html (dc_context_t* context);
int dc_get_push_state (dc_context_t* context);
/**
* Only used by the python tests.
*/
int dc_all_work_done (dc_context_t* context);
// connect
/**
@@ -963,6 +969,54 @@ uint32_t dc_create_chat_by_contact_id (dc_context_t* context, uint32_t co
uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t contact_id);
/**
* Prepare a message for sending.
*
* Call this function if the file to be sent is still in creation.
* Once you're done with creating the file, call dc_send_msg() as usual
* and the message will really be sent.
*
* This is useful as the user can already send the next messages while
* e.g. the recoding of a video is not yet finished. Or the user can even forward
* the message with the file being still in creation to other groups.
*
* Files being sent with the increation-method must be placed in the
* blob directory, see dc_get_blobdir().
* If the increation-method is not used - which is probably the normal case -
* dc_send_msg() copies the file to the blob directory if it is not yet there.
* To distinguish the two cases, msg->state must be set properly. The easiest
* way to ensure this is to re-use the same object for both calls.
*
* Example:
* ~~~
* char* blobdir = dc_get_blobdir(context);
* char* file_to_send = mprintf("%s/%s", blobdir, "send.mp4")
*
* dc_msg_t* msg = dc_msg_new(context, DC_MSG_VIDEO);
* dc_msg_set_file(msg, file_to_send, NULL);
* dc_prepare_msg(context, chat_id, msg);
*
* // ... create the file ...
*
* dc_send_msg(context, chat_id, msg);
*
* dc_msg_unref(msg);
* free(file_to_send);
* dc_str_unref(file_to_send);
* ~~~
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID to send the message to.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id and state of the object are set up,
* The function does not take ownership of the object,
* so you have to free it using dc_msg_unref() as usual.
* @return The ID of the message that is being prepared.
*/
uint32_t dc_prepare_msg (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
/**
* Send a message defined by a dc_msg_t object to a chat.
*
@@ -987,11 +1041,13 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co
* If that fails, is not possible, or the image is already small enough, the image is sent as original.
* If you want images to be always sent as the original file, use the #DC_MSG_FILE type.
*
* Videos and other file types are currently not recoded by the library.
* Videos and other file types are currently not recoded by the library,
* with dc_prepare_msg(), however, you can do that from the UI.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID to send the message to.
* If dc_prepare_msg() was called before, this parameter can be 0.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id of the object is set up,
* The function does not take ownership of the object,
@@ -1008,6 +1064,7 @@ uint32_t dc_send_msg (dc_context_t* context, uint32_t ch
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID to send the message to.
* If dc_prepare_msg() was called before, this parameter can be 0.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id of the object is set up,
* The function does not take ownership of the object,
@@ -3934,7 +3991,7 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
*
* Outgoing message states:
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
* the message enters this state before @ref DC_STATE_OUT_PENDING. Deprecated.
* the message enters this state before @ref DC_STATE_OUT_PENDING.
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
@@ -4144,13 +4201,14 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
* - internet_access:
* true if the Webxdc should get internet access;
* this is the case i.e. for experimental maps integration.
* true if the Webxdc should get full internet access, including Webrtc.
* currently, this is only true for encrypted Webxdc's in the self chat
* that have requested internet access in the manifest.
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
* Should be exposed to `webxdc.sendUpdateMaxSize` in JS land.
+ Should be exposed to `webxdc.sendUpdateMaxSize` in JS land.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
@@ -4484,6 +4542,20 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
*/
char* dc_msg_get_webxdc_href (const dc_msg_t* msg);
/**
* Check if a message is still in creation. A message is in creation between
* the calls to dc_prepare_msg() and dc_send_msg().
*
* Typically, this is used for videos that are recoded by the UI before
* they can be sent.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message is still in creation (dc_send_msg() was not called yet),
* 0=message no longer in creation.
*/
int dc_msg_is_increation (const dc_msg_t* msg);
/**
* Check if the message is an Autocrypt Setup Message.
@@ -4635,7 +4707,7 @@ int dc_msg_has_html (dc_msg_t* msg);
* If the download fails or succeeds,
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
*
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any further download action.
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any futher download action.
* It was fully downloaded, but we failed to decrypt it.
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
*
@@ -5393,8 +5465,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Message containing a sticker, similar to image.
* NB: When sending, the message viewtype may be changed to `Image` by some heuristics like checking
* for transparent pixels.
* If possible, the UI should display the image without borders in a transparent way.
* A click on a sticker will offer to install the sticker set in some future.
*/
@@ -5499,8 +5569,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Outgoing message being prepared. See dc_msg_get_state() for details.
*
* @deprecated 2024-12-07
*/
#define DC_STATE_OUT_PREPARING 18
@@ -5721,23 +5789,6 @@ void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
* returns immediately and once the result is ready it can be retrieved via dc_jsonrpc_next_response()
* the jsonrpc specification defines an invocation id that can then be used to match request and response.
*
* An overview of JSON-RPC calls is available at
* <https://js.jsonrpc.delta.chat/classes/RawClient.html>.
* Note that the page describes only the rough methods.
* Calling convention, casing etc. does vary, this is a known flaw,
* and at some point we will get to improve that :)
*
* Also, note that most calls are more high-level than this CFFI, require more database calls and are slower.
* They're more suitable for an environment that is totally async and/or cannot use CFFI, which might not be true for native apps.
*
* Notable exceptions that exist only as JSON-RPC and probably never get a CFFI counterpart:
* - getMessageReactions(), sendReaction()
* - getHttpResponse()
* - draftSelfReport()
* - getAccountFileSize()
* - importVcard(), parseVcard(), makeVcard()
* - sendWebxdcRealtimeData, sendWebxdcRealtimeAdvertisement(), leaveWebxdcRealtime()
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param request JSON-RPC request as string
@@ -5758,8 +5809,6 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Make a JSON-RPC call and return a response.
*
* See dc_jsonrpc_request() for an overview of possible calls and for more information.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param input JSON-RPC request.
@@ -6850,7 +6899,7 @@ void dc_event_unref(dc_event_t* event);
/// "Failed to send message to %1$s."
///
/// Unused. Was used in group chat status messages.
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact the message cannot be sent to
#define DC_STR_FAILED_SENDING_TO 74

View File

@@ -35,8 +35,6 @@ use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use num_traits::{FromPrimitive, ToPrimitive};
use once_cell::sync::Lazy;
use rand::Rng;
@@ -415,6 +413,16 @@ pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc
block_on(ctx.push_state()) as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_all_work_done()");
return 0;
}
let ctx = &*context;
block_on(async move { ctx.all_work_done().await as libc::c_int })
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_oauth2_url(
context: *mut dc_context_t,
@@ -978,6 +986,27 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_prepare_msg(
context: *mut dc_context_t,
chat_id: u32,
msg: *mut dc_msg_t,
) -> u32 {
if context.is_null() || chat_id == 0 || msg.is_null() {
eprintln!("ignoring careless call to dc_prepare_msg()");
return 0;
}
let ctx = &mut *context;
let ffi_msg: &mut MessageWrapper = &mut *msg;
block_on(async move {
chat::prepare_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message)
.await
.unwrap_or_log_default(ctx, "Failed to prepare message")
})
.to_u32()
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_msg(
context: *mut dc_context_t,
@@ -3694,6 +3723,16 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc
ffi_msg.message.get_webxdc_href().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_is_increation()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.is_increation().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
@@ -4932,97 +4971,105 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
Box::into_raw(Box::new(emitter))
}
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
}
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *mut dc_accounts_t,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
use super::*;
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
}
let account_manager = &*account_manager;
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
account_manager.inner.clone(),
));
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };
Box::into_raw(Box::new(instance))
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_unref(jsonrpc_instance: *mut dc_jsonrpc_instance_t) {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
drop(Box::from_raw(jsonrpc_instance));
}
fn spawn_handle_jsonrpc_request(handle: RpcSession<CommandApi>, request: String) {
spawn(async move {
handle.handle_incoming(&request).await;
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_request(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
request: *const libc::c_char,
) {
if jsonrpc_instance.is_null() || request.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_request()");
return;
}
let handle = &(*jsonrpc_instance).handle;
let request = to_string_lossy(request);
spawn_handle_jsonrpc_request(handle.clone(), request);
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_next_response(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_next_response()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
block_on(api.receiver.recv())
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
input: *const libc::c_char,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
let input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
match res {
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *mut dc_accounts_t,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
}
let account_manager = &*account_manager;
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
account_manager.inner.clone(),
));
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };
Box::into_raw(Box::new(instance))
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_unref(jsonrpc_instance: *mut dc_jsonrpc_instance_t) {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
drop(Box::from_raw(jsonrpc_instance));
}
fn spawn_handle_jsonrpc_request(handle: RpcSession<CommandApi>, request: String) {
spawn(async move {
handle.handle_incoming(&request).await;
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_request(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
request: *const libc::c_char,
) {
if jsonrpc_instance.is_null() || request.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_request()");
return;
}
let handle = &(*jsonrpc_instance).handle;
let request = to_string_lossy(request);
spawn_handle_jsonrpc_request(handle.clone(), request);
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_next_response(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_next_response()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
block_on(api.receiver.recv())
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
input: *const libc::c_char,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
let input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
match res {
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
}
None => ptr::null_mut(),
}
None => ptr::null_mut(),
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.154.1"
version = "1.151.2"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -33,7 +33,7 @@ base64 = { workspace = true }
# optional dependencies
axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.11.6", optional = true }
env_logger = { version = "0.11.5", optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }

View File

@@ -1767,7 +1767,7 @@ impl CommandApi {
account_id: u32,
instance_msg_id: u32,
update_str: String,
_descr: Option<String>,
_descr: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str)
@@ -1829,18 +1829,6 @@ impl CommandApi {
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
}
/// Get href from a WebxdcInfoMessage which might include a hash holding
/// information about a specific position or state in a webxdc app (optional)
async fn get_webxdc_href(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
Ok(message.get_webxdc_href())
}
/// Get blob encoded as base64 from a webxdc message
///
/// path is the path of the file within webxdc archive

View File

@@ -69,7 +69,7 @@ pub enum EventType {
/// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a message box then.
/// in a messasge box then.
Error { msg: String },
/// An action cannot be performed because the user is not in the group.
@@ -109,7 +109,6 @@ pub enum EventType {
/// Incoming webxdc info or summary update, should be notified.
#[serde(rename_all = "camelCase")]
IncomingWebxdcNotify {
chat_id: u32,
contact_id: u32,
msg_id: u32,
text: String,
@@ -344,13 +343,11 @@ impl From<CoreEventType> for EventType {
reaction: reaction.as_str().to_string(),
},
CoreEventType::IncomingWebxdcNotify {
chat_id,
contact_id,
msg_id,
text,
href,
} => IncomingWebxdcNotify {
chat_id: chat_id.to_u32(),
contact_id: contact_id.to_u32(),
msg_id: msg_id.to_u32(),
text,

View File

@@ -85,8 +85,6 @@ pub struct MessageObject {
webxdc_info: Option<WebxdcMessageInfo>,
webxdc_href: Option<String>,
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
@@ -243,10 +241,6 @@ impl MessageObject {
file_name: message.get_filename(),
webxdc_info,
// On a WebxdcInfoMessage this might include a hash holding
// information about a specific position or state in a webxdc app
webxdc_href: message.get_webxdc_href(),
download_state,
reactions,
@@ -273,9 +267,6 @@ pub enum MessageViewtype {
Gif,
/// Message containing a sticker, similar to image.
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
///
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker,

View File

@@ -57,7 +57,6 @@ impl WebxdcMessageInfo {
document,
summary,
source_code_url,
request_integration: _,
internet_access,
self_addr,
send_update_interval,

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.154.1"
version = "1.151.2"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -13,6 +13,7 @@ classifiers = [
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
@@ -23,7 +24,6 @@ classifiers = [
"Topic :: Communications :: Email"
]
readme = "README.md"
requires-python = ">=3.8"
[tool.setuptools.package-data]
deltachat_rpc_client = [

View File

@@ -41,7 +41,6 @@ class EventType(str, Enum):
REACTIONS_CHANGED = "ReactionsChanged"
INCOMING_MSG = "IncomingMsg"
INCOMING_MSG_BUNCH = "IncomingMsgBunch"
INCOMING_REACTION = "IncomingReaction"
MSGS_NOTICED = "MsgsNoticed"
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"

View File

@@ -131,7 +131,10 @@ class Rpc:
def reader_loop(self) -> None:
try:
while line := self.process.stdout.readline():
while True:
line = self.process.stdout.readline()
if not line: # EOF
break
response = json.loads(line)
if "id" in response:
response_id = response["id"]
@@ -147,7 +150,10 @@ class Rpc:
def writer_loop(self) -> None:
"""Writer loop ensuring only a single thread writes requests."""
try:
while request := self.request_queue.get():
while True:
request = self.request_queue.get()
if not request:
break
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data)
self.process.stdin.flush()

View File

@@ -73,25 +73,22 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
# Alice deletes "vg-request".
while True:
event = alice.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
alice.wait_for_securejoin_inviter_success()
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
# Check that at least some of the handshake messages are deleted.
for ac in [alice, bob]:
while True:
event = ac.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
bob.wait_for_securejoin_joiner_success()
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected == protect

View File

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

View File

@@ -65,13 +65,13 @@ so by default it uses the prebuilds.
- this will run `update_optional_dependencies_and_version.js` (in the `prepack` script),
which puts all platform packages into `optionalDependencies` and updates the `version` in `package.json`
## How to build a version you can use locally on your host machine for development
## How to build a version you can use localy on your host machine for development
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have separate scripts for making it work for local installation.
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have seperate scripts for making it work for local installation.
- If you just need your host platform run `python scripts/make_local_dev_version.py`
- note: this clears the `platform_package` folder
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple platforms with `build_platform_package.py`
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple plaftorms with `build_platform_package.py`
## Thanks to nlnet

View File

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

View File

@@ -6,7 +6,7 @@ const expected_cwd = join(dirname(fileURLToPath(import.meta.url)), "..");
if (process.cwd() !== expected_cwd) {
console.error(
"CWD mismatch: this script needs to be run from " + expected_cwd,
"CWD missmatch: this script needs to be run from " + expected_cwd,
{ actual: process.cwd(), expected: expected_cwd }
);
process.exit(1);
@@ -40,7 +40,7 @@ const platform_package_names = await Promise.all(
"has a different version than the version of the rpc server.",
{ rpc_server: version, platform_package: p.version }
);
throw new Error("version mismatch");
throw new Error("version missmatch");
}
return { folder_name: name, package_name: p.name };
})

View File

@@ -66,7 +66,7 @@ async fn main_impl() -> Result<()> {
// Logs from `log` crate and traces from `tracing` crate
// are configurable with `RUST_LOG` environment variable
// and go to stderr to avoid interfering with JSON-RPC using stdout.
// and go to stderr to avoid interferring with JSON-RPC using stdout.
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)

View File

@@ -18,9 +18,6 @@ ignore = [
# Unmaintained instant
"RUSTSEC-2024-0384",
# idna 0.5.0
"RUSTSEC-2024-0421",
]
[bans]
@@ -40,8 +37,10 @@ skip = [
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "http", version = "0.2.12" },
{ name = "idna", version = "0.5.0" },
{ name = "nix", version = "0.26.4" },
{ name = "num_enum_derive", version = "0.5.11" },
{ name = "num_enum", version = "0.5.11" },
{ name = "proc-macro-crate", version = "1.3.1" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
@@ -51,9 +50,8 @@ skip = [
{ name = "regex-syntax", version = "0.6.29" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "time", version = "<0.3" },
{ name = "toml_edit", version = "0.19.15" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
{ name = "windows_aarch64_msvc", version = "<0.52" },
@@ -66,6 +64,7 @@ skip = [
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
{ name = "winreg", version = "0.50.0" },
]
@@ -82,7 +81,6 @@ allow = [
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
]

62
flake.lock generated
View File

@@ -47,17 +47,16 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1711088506,
"narHash": "sha256-USdlY7Tx2oJWqFBpp10+03+h7eVhpkQ4s9t1ERjeIJE=",
"lastModified": 1731393059,
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
"owner": "nix-community",
"repo": "fenix",
"rev": "85f4139f3c092cf4afd9f9906d7ed218ef262c97",
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"rev": "85f4139f3c092cf4afd9f9906d7ed218ef262c97",
"type": "github"
}
},
@@ -115,25 +114,6 @@
"type": "github"
}
},
"new-fenix": {
"inputs": {
"nixpkgs": "nixpkgs_4",
"rust-analyzer-src": "rust-analyzer-src_2"
},
"locked": {
"lastModified": 1734417396,
"narHash": "sha256-32x1Z+Pz3Jv0cK9EG56cFTKXy/mZ/c+Ikxw+aVfKHp4=",
"owner": "nix-community",
"repo": "fenix",
"rev": "a18d41b26e998e95a598858fdb86ba22fb5da47d",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1730207686,
@@ -194,22 +174,6 @@
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1734119587,
"narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
@@ -231,9 +195,8 @@
"fenix": "fenix",
"flake-utils": "flake-utils_2",
"naersk": "naersk",
"new-fenix": "new-fenix",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_5"
"nixpkgs": "nixpkgs_4"
}
},
"rust-analyzer-src": {
@@ -253,23 +216,6 @@
"type": "github"
}
},
"rust-analyzer-src_2": {
"flake": false,
"locked": {
"lastModified": 1734386068,
"narHash": "sha256-Py025JiD9lcPmldB7X1AEjq3WBTS60jZUJRtTDonmaE=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "0a706f7d2ac093985eae317781200689cfd48b78",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,

View File

@@ -1,19 +1,14 @@
{
description = "Delta Chat core";
inputs = {
# Old Rust to build releases.
fenix.url = "github:nix-community/fenix?rev=85f4139f3c092cf4afd9f9906d7ed218ef262c97";
# New Rust for development shell.
new-fenix.url = "github:nix-community/fenix";
fenix.url = "github:nix-community/fenix";
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
nix-filter.url = "github:numtide/nix-filter";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
android.url = "github:tadfisher/android-nixpkgs";
};
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, new-fenix, android }:
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
@@ -544,13 +539,13 @@
let
pkgs = import nixpkgs {
system = system;
overlays = [ new-fenix.overlays.default ];
overlays = [ fenix.overlays.default ];
};
in
pkgs.mkShell {
buildInputs = with pkgs; [
(new-fenix.packages.${system}.complete.withComponents [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"

270
fuzz/Cargo.lock generated
View File

@@ -146,7 +146,6 @@ dependencies = [
"blake2",
"cpufeatures",
"password-hash",
"zeroize",
]
[[package]]
@@ -179,7 +178,7 @@ dependencies = [
"nom",
"num-traits",
"rusticata-macros",
"thiserror 1.0.58",
"thiserror",
"time 0.3.36",
]
@@ -191,7 +190,7 @@ checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
"synstructure 0.13.1",
]
@@ -203,7 +202,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -275,7 +274,7 @@ dependencies = [
"pin-utils",
"self_cell",
"stop-token",
"thiserror 1.0.58",
"thiserror",
"tokio",
]
@@ -286,7 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec"
dependencies = [
"native-tls",
"thiserror 1.0.58",
"thiserror",
"tokio",
"url",
]
@@ -299,22 +298,23 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
name = "async-smtp"
version = "0.10.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee04bcf0a7ebf5594f9aff84935dc8cb0490b65055913a7a4c4d08f81e181d6"
checksum = "8709c0d4432be428a88a06746689a9cb543e8e27ef7f61ca4d0455003a3d8c5b"
dependencies = [
"anyhow",
"base64 0.13.1",
"futures",
"hostname",
"log",
"nom",
"pin-project",
"thiserror 1.0.58",
"thiserror",
"tokio",
]
@@ -326,7 +326,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -339,7 +339,7 @@ dependencies = [
"crc32fast",
"futures-lite 2.5.0",
"pin-project",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tokio-util",
]
@@ -1040,7 +1040,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1108,7 +1108,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1119,7 +1119,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1152,7 +1152,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.151.5"
version = "1.150.0"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1220,7 +1220,7 @@ dependencies = [
"strum_macros",
"tagger",
"textwrap",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tokio-io-timeout",
"tokio-rustls",
@@ -1263,7 +1263,7 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1300,7 +1300,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1331,7 +1331,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1341,7 +1341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
dependencies = [
"derive_builder_core",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1361,7 +1361,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
"unicode-xid",
]
@@ -1579,7 +1579,7 @@ dependencies = [
"hex",
"lazy_static",
"regex",
"thiserror 1.0.58",
"thiserror",
]
[[package]]
@@ -1670,7 +1670,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1690,7 +1690,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -1814,7 +1814,7 @@ dependencies = [
"anyhow",
"async-trait",
"log",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tokio-stream",
]
@@ -2061,7 +2061,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -2331,7 +2331,7 @@ dependencies = [
"ipnet",
"once_cell",
"rand 0.8.5",
"thiserror 1.0.58",
"thiserror",
"time 0.3.36",
"tinyvec",
"tokio",
@@ -2355,7 +2355,7 @@ dependencies = [
"rand 0.8.5",
"resolv-conf",
"smallvec",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tracing",
]
@@ -2718,7 +2718,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"ssh-key",
"thiserror 1.0.58",
"thiserror",
"ttl_cache",
"url",
"zeroize",
@@ -2828,7 +2828,7 @@ dependencies = [
"netlink-packet-route",
"netlink-sys",
"netwatch",
"num_enum",
"num_enum 0.7.2",
"once_cell",
"parking_lot",
"pin-project",
@@ -2848,7 +2848,7 @@ dependencies = [
"strum",
"stun-rs",
"surge-ping",
"thiserror 1.0.58",
"thiserror",
"time 0.3.36",
"tokio",
"tokio-rustls",
@@ -2880,7 +2880,7 @@ dependencies = [
"rustc-hash 2.0.0",
"rustls",
"socket2 0.5.6",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tracing",
]
@@ -2898,7 +2898,7 @@ dependencies = [
"rustls",
"rustls-platform-verifier",
"slab",
"thiserror 1.0.58",
"thiserror",
"tinyvec",
"tracing",
]
@@ -2954,7 +2954,7 @@ dependencies = [
"combine",
"jni-sys",
"log",
"thiserror 1.0.58",
"thiserror",
"walkdir",
]
@@ -3186,7 +3186,7 @@ dependencies = [
"serde_bencode",
"serde_bytes",
"sha1_smol",
"thiserror 1.0.58",
"thiserror",
"tracing",
]
@@ -3349,7 +3349,7 @@ dependencies = [
"anyhow",
"byteorder",
"paste",
"thiserror 1.0.58",
"thiserror",
]
[[package]]
@@ -3363,7 +3363,7 @@ dependencies = [
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 1.0.58",
"thiserror",
"tokio",
]
@@ -3401,7 +3401,7 @@ dependencies = [
"rtnetlink",
"serde",
"socket2 0.5.6",
"thiserror 1.0.58",
"thiserror",
"time 0.3.36",
"tokio",
"tracing",
@@ -3500,7 +3500,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -3543,13 +3543,34 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
dependencies = [
"num_enum_derive 0.5.11",
]
[[package]]
name = "num_enum"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
dependencies = [
"num_enum_derive",
"num_enum_derive 0.7.2",
]
[[package]]
name = "num_enum_derive"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 1.0.107",
]
[[package]]
@@ -3558,10 +3579,10 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate",
"proc-macro-crate 3.1.0",
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -3792,7 +3813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
dependencies = [
"memchr",
"thiserror 1.0.58",
"thiserror",
"ucd-trie",
]
@@ -3816,7 +3837,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -3832,15 +3853,15 @@ dependencies = [
[[package]]
name = "pgp"
version = "0.14.2"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1877a97fd422433220ad272eb008ec55691944b1200e9eb204e3cb2cb69d34e9"
checksum = "49bb5f77aaf8ae1ed6fe63387ad513b10cd44716fd053ecc227b9493c096cdb2"
dependencies = [
"aes",
"aes-gcm",
"aes-kw",
"argon2",
"base64 0.22.1",
"base64 0.21.7",
"bitfield",
"block-padding",
"blowfish",
@@ -3876,7 +3897,7 @@ dependencies = [
"nom",
"num-bigint-dig",
"num-traits",
"num_enum",
"num_enum 0.5.11",
"ocb3",
"p256",
"p384",
@@ -3890,7 +3911,7 @@ dependencies = [
"sha3",
"signature",
"smallvec",
"thiserror 1.0.58",
"thiserror",
"twofish",
"x25519-dalek",
"x448",
@@ -3914,7 +3935,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -3946,7 +3967,7 @@ dependencies = [
"mainline",
"self_cell",
"simple-dns",
"thiserror 1.0.58",
"thiserror",
"tracing",
"ureq",
"wasm-bindgen",
@@ -4000,7 +4021,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -4081,12 +4102,12 @@ dependencies = [
"iroh-metrics",
"libc",
"netwatch",
"num_enum",
"num_enum 0.7.2",
"rand 0.8.5",
"serde",
"smallvec",
"socket2 0.5.6",
"thiserror 1.0.58",
"thiserror",
"time 0.3.36",
"tokio",
"tokio-util",
@@ -4178,13 +4199,23 @@ dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit 0.19.15",
]
[[package]]
name = "proc-macro-crate"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"toml_edit",
"toml_edit 0.21.0",
]
[[package]]
@@ -4221,9 +4252,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.92"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
@@ -4248,7 +4279,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -4305,29 +4336,26 @@ dependencies = [
"quinn-udp",
"rustc-hash 1.1.0",
"rustls",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "quinn-proto"
version = "0.11.9"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe"
dependencies = [
"bytes",
"getrandom 0.2.11",
"rand 0.8.5",
"ring",
"rustc-hash 2.0.0",
"rustc-hash 1.1.0",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.6",
"thiserror",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
@@ -4640,21 +4668,22 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.7"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
checksum = "72f1471dbb4be5de45050e8ef7040625298ccb9efe941419ac2697088715925f"
dependencies = [
"byteorder",
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-iter",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"sha2",
"signature",
"spki",
"subtle",
"zeroize",
]
@@ -4673,7 +4702,7 @@ dependencies = [
"netlink-proto",
"netlink-sys",
"nix",
"thiserror 1.0.58",
"thiserror",
"tokio",
]
@@ -4762,9 +4791,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.19"
version = "0.23.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1"
checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f"
dependencies = [
"log",
"once_cell",
@@ -4803,9 +4832,6 @@ name = "rustls-pki-types"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
dependencies = [
"web-time",
]
[[package]]
name = "rustls-platform-verifier"
@@ -5024,7 +5050,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -5099,7 +5125,6 @@ checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423"
dependencies = [
"digest",
"sha1",
"zeroize",
]
[[package]]
@@ -5157,7 +5182,7 @@ dependencies = [
"shadowsocks-crypto",
"socket2 0.5.6",
"spin 0.9.8",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tokio-tfo",
"url",
@@ -5303,9 +5328,9 @@ dependencies = [
[[package]]
name = "spki"
version = "0.7.3"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
checksum = "37a5be806ab6f127c3da44b7378837ebf01dadca8510a0e572460216b228bd0e"
dependencies = [
"base64ct",
"der",
@@ -5390,7 +5415,7 @@ dependencies = [
"proc-macro2",
"quote",
"struct_iterable_internal",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -5418,7 +5443,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -5462,7 +5487,7 @@ dependencies = [
"pnet_packet",
"rand 0.8.5",
"socket2 0.5.6",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tracing",
]
@@ -5480,9 +5505,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.90"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2",
"quote",
@@ -5526,7 +5551,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -5601,16 +5626,7 @@ version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [
"thiserror-impl 1.0.58",
]
[[package]]
name = "thiserror"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
dependencies = [
"thiserror-impl 2.0.6",
"thiserror-impl",
]
[[package]]
@@ -5621,18 +5637,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "thiserror-impl"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -5738,7 +5743,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -5818,7 +5823,7 @@ dependencies = [
"http 1.1.0",
"httparse",
"js-sys",
"thiserror 1.0.58",
"thiserror",
"tokio",
"tokio-tungstenite",
"wasm-bindgen",
@@ -5850,7 +5855,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
"toml_edit 0.21.0",
]
[[package]]
@@ -5862,6 +5867,17 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.21.0"
@@ -5901,7 +5917,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -5972,7 +5988,7 @@ dependencies = [
"log",
"rand 0.8.5",
"sha1",
"thiserror 1.0.58",
"thiserror",
"url",
"utf-8",
]
@@ -6201,7 +6217,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
"wasm-bindgen-shared",
]
@@ -6235,7 +6251,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -6255,7 +6271,7 @@ dependencies = [
"event-listener 4.0.3",
"futures-util",
"parking_lot",
"thiserror 1.0.58",
"thiserror",
]
[[package]]
@@ -6268,16 +6284,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.7"
@@ -6387,7 +6393,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -6398,7 +6404,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]
@@ -6687,7 +6693,7 @@ dependencies = [
"futures",
"log",
"serde",
"thiserror 1.0.58",
"thiserror",
"windows 0.52.0",
]
@@ -6736,7 +6742,7 @@ dependencies = [
"nom",
"oid-registry",
"rusticata-macros",
"thiserror 1.0.58",
"thiserror",
"time 0.3.36",
]
@@ -6796,7 +6802,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
"syn 2.0.52",
]
[[package]]

View File

@@ -299,6 +299,10 @@ export class Message {
return Boolean(binding.dcn_msg_is_forwarded(this.dc_msg))
}
isIncreation() {
return Boolean(binding.dcn_msg_is_increation(this.dc_msg))
}
isInfo() {
return Boolean(binding.dcn_msg_is_info(this.dc_msg))
}

View File

@@ -9,7 +9,7 @@ const buildArgs = [
'build',
'--release',
'--features',
'vendored',
'vendored,jsonrpc',
'-p',
'deltachat_ffi'
]

View File

@@ -2374,6 +2374,17 @@ NAPI_METHOD(dcn_msg_is_forwarded) {
NAPI_RETURN_INT32(is_forwarded);
}
NAPI_METHOD(dcn_msg_is_increation) {
NAPI_ARGV(1);
NAPI_DC_MSG();
//TRACE("calling..");
int is_increation = dc_msg_is_increation(dc_msg);
//TRACE("result %d", is_increation);
NAPI_RETURN_INT32(is_increation);
}
NAPI_METHOD(dcn_msg_is_info) {
NAPI_ARGV(1);
NAPI_DC_MSG();
@@ -3544,6 +3555,7 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_msg_has_location);
NAPI_EXPORT_FUNCTION(dcn_msg_has_html);
NAPI_EXPORT_FUNCTION(dcn_msg_is_forwarded);
NAPI_EXPORT_FUNCTION(dcn_msg_is_increation);
NAPI_EXPORT_FUNCTION(dcn_msg_is_info);
NAPI_EXPORT_FUNCTION(dcn_msg_is_sent);
NAPI_EXPORT_FUNCTION(dcn_msg_is_setupmessage);

View File

@@ -536,6 +536,7 @@ describe('Offline Tests with unconfigured account', function () {
strictEqual(msg.getWidth(), 0, 'no message width')
strictEqual(msg.isDeadDrop(), false, 'not deaddrop')
strictEqual(msg.isForwarded(), false, 'not forwarded')
strictEqual(msg.isIncreation(), false, 'not in creation')
strictEqual(msg.isInfo(), false, 'not an info message')
strictEqual(msg.isSent(), false, 'messge is not sent')
strictEqual(msg.isSetupmessage(), false, 'not an autocrypt setup message')

View File

@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.154.1"
"version": "1.151.2"
}

View File

@@ -52,7 +52,10 @@ python3-venv` should give you a usable python installation.
First, build the core library::
cargo build --release -p deltachat_ffi
cargo build --release -p deltachat_ffi --features jsonrpc
`jsonrpc` feature is required even if not used by the bindings
because `deltachat.h` includes JSON-RPC functions unconditionally.
Create the virtual environment and activate it::

View File

@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.154.1"
version = "1.151.2"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"
requires-python = ">=3.7"
authors = [
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
]

View File

@@ -671,6 +671,9 @@ class Account:
def get_connectivity_html(self) -> str:
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
def all_work_done(self):
return lib.dc_all_work_done(self._dc_context)
def start_io(self):
"""start this account's IO scheduling (Rust-core async scheduler).

View File

@@ -271,7 +271,8 @@ class Chat:
:param msg: a :class:`deltachat.message.Message` instance
previously returned by
e.g. :meth:`deltachat.message.Message.new_empty`.
e.g. :meth:`deltachat.message.Message.new_empty` or
:meth:`prepare_file`.
:raises ValueError: if message can not be sent.
:returns: a :class:`deltachat.message.Message` instance as
@@ -340,6 +341,37 @@ class Chat:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
def prepare_message(self, msg):
"""prepare a message for sending.
:param msg: the message to be prepared.
:returns: a :class:`deltachat.message.Message` instance.
This is the same object that was passed in, which
has been modified with the new state of the core.
"""
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
if msg_id == 0:
raise ValueError("message could not be prepared")
# modify message in place to avoid bad state for the caller
msg._dc_msg = Message.from_db(self.account, msg_id)._dc_msg
return msg
def prepare_message_file(self, path, mime_type=None, view_type="file"):
"""prepare a message for sending and return the resulting Message instance.
To actually send the message, call :meth:`send_prepared`.
The file must be inside the blob directory.
:param path: path to the file.
:param mime_type: the mime-type of this file, defaults to auto-detection.
:param view_type: "text", "image", "gif", "audio", "video", "file"
:raises ValueError: if message can not be prepared/chat does not exist.
:returns: the resulting :class:`Message` instance
"""
msg = Message.new_empty(self.account, view_type)
msg.set_file(path, mime_type)
return self.prepare_message(msg)
def send_prepared(self, message):
"""send a previously prepared message.

View File

@@ -158,6 +158,12 @@ class FFIEventTracker:
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def wait_for_all_work_done(self):
while True:
if self.account.all_work_done():
return
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile(f"(?:{event_name_regex}).*")

View File

@@ -152,7 +152,7 @@ class TestProcess:
def get_liveconfig_producer(self):
"""provide live account configs, cached on a per-test-process scope
so that test functions can reuse already known live configs.
so that test functions can re-use already known live configs.
"""
chatmail_opt = self.pytestconfig.getoption("--chatmail")
if chatmail_opt:

View File

@@ -1253,10 +1253,7 @@ def test_no_old_msg_is_fresh(acfactory, lp):
def test_prefer_encrypt(acfactory, lp):
"""Test quorum rule for encryption preference in 1:1 and group chat."""
ac1 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
ac2 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
ac3 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
acfactory.bring_accounts_online()
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1.set_config("e2ee_enabled", "0")
ac2.set_config("e2ee_enabled", "1")
ac3.set_config("e2ee_enabled", "0")
@@ -1279,8 +1276,7 @@ def test_prefer_encrypt(acfactory, lp):
lp.sec("ac2: sending message to ac1")
chat2 = ac2.create_chat(ac1)
msg2 = chat2.send_text("message2")
# Own preference is `Mutual` and we have the peer's key.
assert msg2.is_encrypted()
assert not msg2.is_encrypted()
ac1._evtracker.wait_next_incoming_message()
lp.sec("ac1: sending message to group chat with ac2 and ac3")
@@ -1296,8 +1292,8 @@ def test_prefer_encrypt(acfactory, lp):
ac3.set_config("e2ee_enabled", "1")
chat3 = ac3.create_chat(ac1)
msg4 = chat3.send_text("message4")
# Own preference is `Mutual` and we have the peer's key.
assert msg4.is_encrypted()
# ac1 still does not prefer encryption
assert not msg4.is_encrypted()
ac1._evtracker.wait_next_incoming_message()
lp.sec("ac1: sending another message to group chat with ac2 and ac3")
@@ -1370,9 +1366,10 @@ def test_quote_encrypted(acfactory, lp):
msg_draft.quote = quoted_msg
chat.set_draft(msg_draft)
# Get the draft and send it.
# Get the draft, prepare and send it.
msg_draft = chat.get_draft()
chat.send_msg(msg_draft)
msg_out = chat.prepare_message(msg_draft)
chat.send_prepared(msg_out)
chat.set_draft(None)
assert chat.get_draft() is None
@@ -1902,11 +1899,10 @@ def test_connectivity(acfactory, lp):
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `DC_CONNECTIVITY_CONNECTED`, "
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
"all messages are fetched",
)
@@ -1915,7 +1911,7 @@ def test_connectivity(acfactory, lp):
ac2.create_chat(ac1).send_text("Hi")
idle1.wait_for_new_message()
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_all_work_done()
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1
assert msgs[0].text == "Hi"
@@ -1931,6 +1927,30 @@ def test_connectivity(acfactory, lp):
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages")
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked")
ac1.create_contact(ac2).block()
ac1.direct_imap.select_config_folder("inbox")
with ac1.direct_imap.idle() as idle1:
ac2.create_chat(ac1).send_text("Hi")
idle1.wait_for_new_message()
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
ac1.set_config("configured_mail_pw", "abc")
@@ -1941,6 +1961,32 @@ def test_connectivity(acfactory, lp):
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_all_work_done(acfactory, lp):
"""
Tests that calling start_io() immediately followed by maybe_network()
and then waiting for all_work_done() reliably fetches the messages
delivered while account was offline.
In other words, connectivity should not change to a state
where all_work_done() returns true until IMAP connection goes idle.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.direct_imap.select_config_folder("inbox")
with ac1.direct_imap.idle() as idle1:
ac2.create_chat(ac1).send_text("Hi")
idle1.wait_for_new_message()
ac1.start_io()
ac1.maybe_network()
ac1._evtracker.wait_for_all_work_done()
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1
assert msgs[0].text == "Hi"
def test_fetch_deleted_msg(acfactory, lp):
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
hundreds of times, because uid_next was not updated.
@@ -2294,8 +2340,9 @@ def test_group_quote(acfactory, lp):
reply_msg = Message.new_empty(msg.chat.account, "text")
reply_msg.set_text("reply")
reply_msg.quote = msg
reply_msg = msg.chat.prepare_message(reply_msg)
assert reply_msg.quoted_text == "hello"
msg.chat.send_msg(reply_msg)
msg.chat.send_prepared(reply_msg)
lp.sec("ac3: receiving reply")
received_reply = ac3._evtracker.wait_next_incoming_message()

View File

@@ -0,0 +1,107 @@
import os.path
import shutil
from filecmp import cmp
import pytest
def wait_msg_delivered(account, msg_list):
"""wait for one or more MSG_DELIVERED events to match msg_list contents."""
msg_list = list(msg_list)
while msg_list:
ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
msg_list.remove((ev.data1, ev.data2))
def wait_msgs_changed(account, msgs_list):
"""wait for one or more MSGS_CHANGED events to match msgs_list contents."""
account.log(f"waiting for msgs_list={msgs_list}")
msgs_list = list(msgs_list)
while msgs_list:
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
for i, (data1, data2) in enumerate(msgs_list):
if ev.data1 == data1:
if data2 is None or ev.data2 == data2:
del msgs_list[i]
break
else:
account.log(f"waiting mismatch data1={data1} data2={data2}")
return ev.data2
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
lp.sec("Creating in-creation file outside of blobdir")
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.touch()
with pytest.raises(Exception):
chat.prepare_message_file(str(src))
def test_no_increation_copies_to_blobdir(self, tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
lp.sec("Creating file outside of blobdir")
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.write_text("hello there\n")
msg = chat.send_file(str(src))
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
assert msg.filename.endswith(".txt")
def test_forward_increation(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
lp.sec("create a message with a file in creation")
orig = data.get_path("d.png")
path = os.path.join(ac1.get_blobdir(), "d.png")
with open(path, "x") as fp:
fp.write("preparing")
prepared_original = chat.prepare_message_file(path)
assert prepared_original.is_out_preparing()
wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
lp.sec("create a new group")
chat2 = ac1.create_group_chat("newgroup")
wait_msgs_changed(ac1, [(0, 0)])
lp.sec("add a contact to new group")
chat2.add_contact(ac2)
wait_msgs_changed(ac1, [(chat2.id, None)])
lp.sec("forward the message while still in creation")
ac1.forward_messages([prepared_original], chat2)
forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
forwarded_msg = ac1.get_message_by_id(forwarded_id)
assert forwarded_msg.is_out_preparing()
lp.sec("finish creating the file and send it")
assert prepared_original.is_out_preparing()
shutil.copyfile(orig, path)
chat.send_prepared(prepared_original)
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
lp.sec("check that both forwarded and original message are proper.")
wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
fwd_msg = ac1.get_message_by_id(forwarded_id)
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
lp.sec("wait for both messages to be delivered to SMTP")
wait_msg_delivered(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
lp.sec("wait1 for original or forwarded messages to arrive")
received_original = ac2._evtracker.wait_next_incoming_message()
assert cmp(received_original.filename, orig, shallow=False)
lp.sec("wait2 for original or forwarded messages to arrive")
received_copy = ac2._evtracker.wait_next_incoming_message()
assert received_copy.id != received_original.id
assert cmp(received_copy.filename, orig, shallow=False)

View File

@@ -378,6 +378,30 @@ class TestOfflineChat:
with pytest.raises(ValueError):
chat1.send_text("msg1")
def test_prepare_message_and_send(self, ac1, chat1):
msg = chat1.prepare_message(Message.new_empty(chat1.account, "text"))
msg.set_text("hello world")
assert msg.text == "hello world"
assert msg.id > 0
chat1.send_prepared(msg)
assert "Sent" in msg.get_message_info()
str(msg)
repr(msg)
assert msg == ac1.get_message_by_id(msg.id)
def test_prepare_file(self, ac1, chat1):
blobdir = ac1.get_blobdir()
p = os.path.join(blobdir, "somedata.txt")
with open(p, "w") as f:
f.write("some data")
message = chat1.prepare_message_file(p)
assert message.id > 0
message.set_text("hello world")
assert message.is_out_preparing()
assert message.text == "hello world"
chat1.send_prepared(message)
assert "Sent" in message.get_message_info()
def test_message_eq_contains(self, chat1):
msg = chat1.send_text("msg1")
msg2 = None
@@ -667,7 +691,8 @@ class TestOfflineChat:
assert os.path.exists(messages[1].filename)
def test_set_get_draft(self, chat1):
msg1 = Message.new_empty(chat1.account, "text")
msg = Message.new_empty(chat1.account, "text")
msg1 = chat1.prepare_message(msg)
msg1.set_text("hello")
chat1.set_draft(msg1)
msg1.set_text("obsolete")
@@ -686,6 +711,21 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
def test_quote(self, chat1):
"""Offline quoting test"""
msg = Message.new_empty(chat1.account, "text")
msg.set_text("Multi\nline\nmessage")
assert msg.quoted_text is None
# Prepare message to assign it a Message-Id.
# Messages without Message-Id cannot be quoted.
msg = chat1.prepare_message(msg)
reply_msg = Message.new_empty(chat1.account, "text")
reply_msg.set_text("reply")
reply_msg.quote = msg
assert reply_msg.quoted_text == "Multi\nline\nmessage"
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")

View File

@@ -212,13 +212,8 @@ def test_logged_ac_process_ffi_failure(acfactory):
0 / 0
cap = Queue()
# Make sure the next attempt to log an event fails.
ac1.add_account_plugin(FailPlugin())
# Start capturing events.
ac1.log = cap.put
ac1.add_account_plugin(FailPlugin())
# cause any event eg contact added/changed
ac1.create_contact("something@example.org")
res = cap.get(timeout=10)

View File

@@ -1 +1 @@
2025-01-15
2024-11-26

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.84.0
RUST_VERSION=1.82.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -11,7 +11,7 @@ set -euo pipefail
export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi
cargo build -p deltachat_ffi --features jsonrpc
tox -c python -e py --devenv venv
venv/bin/pip install --upgrade pip

View File

@@ -12,7 +12,7 @@ export DCC_RS_DEV=`pwd`
cd python
cargo build -p deltachat_ffi
cargo build -p deltachat_ffi --features jsonrpc
# remove and inhibit writing PYC files
rm -rf tests/__pycache__

View File

@@ -8,7 +8,7 @@ set -e -x
# compile core lib
cargo build --release -p deltachat_ffi
cargo build --release -p deltachat_ffi --features jsonrpc
# Statically link against libdeltachat.a.
export DCC_RS_DEV="$PWD"
@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py38,py39,py310,py311,py312,py313,pypy38,pypy39,pypy310 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,py313,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

View File

@@ -279,9 +279,7 @@ impl<'a> BlobObject<'a> {
let ext: String = name
.chars()
.rev()
.take_while(|c| {
(!c.is_ascii_punctuation() || *c == '.') && !c.is_whitespace() && !c.is_control()
})
.take_while(|c| !c.is_whitespace())
.take(33)
.collect::<Vec<_>>()
.iter()
@@ -765,6 +763,7 @@ mod tests {
use fs::File;
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
use crate::message::{Message, Viewtype};
use crate::test_utils::{self, TestContext};
@@ -984,10 +983,6 @@ mod tests {
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
assert_eq!(stem, "a. tar");
assert_eq!(ext, ".tar.gz");
let (stem, ext) = BlobObject::sanitise_name("Guia_uso_GNB (v0.8).pdf");
assert_eq!(stem, "Guia_uso_GNB (v0.8)");
assert_eq!(ext, ".pdf");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1463,21 +1458,36 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_gif_as_sticker() -> Result<()> {
let bytes = include_bytes!("../test-data/image/image100x50.gif");
let alice = &TestContext::new_alice().await;
let file = alice.get_blobdir().join("file").with_extension("gif");
fs::write(&file, &bytes)
.await
.context("failed to write file")?;
let mut msg = Message::new(Viewtype::Sticker);
async fn test_increation_in_blobdir() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let file = t.get_blobdir().join("anyfile.dat");
fs::write(&file, b"bla").await?;
let mut msg = Message::new(Viewtype::File);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.get_self_chat().await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
// extension.
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
assert_eq!(prepared_id, msg.id);
assert!(msg.is_increation());
let msg = Message::load_from_db(&t, prepared_id).await?;
assert!(msg.is_increation());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_increation_not_blobdir() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
let file = t.dir.path().join("anyfile.dat");
fs::write(&file, b"bla").await?;
let mut msg = Message::new(Viewtype::File);
msg.set_file(file.to_str().unwrap(), None);
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -144,7 +144,7 @@ impl Chatlist {
ORDER BY timestamp DESC, id DESC LIMIT 1)
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)
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
@@ -261,7 +261,7 @@ impl Chatlist {
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))
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?))
GROUP BY c.id
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(
@@ -550,7 +550,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
assert!(chats.len() == 1);
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await
@@ -576,7 +576,7 @@ mod tests {
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
assert!(chats.len() == 3);
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap()
@@ -585,7 +585,7 @@ mod tests {
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 2); // device chat cannot be written and is skipped on forwarding
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
assert!(Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap()
@@ -597,7 +597,7 @@ mod tests {
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
assert!(chats.len() == 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -80,7 +80,7 @@ pub enum Config {
/// SMTP server security (e.g. TLS, STARTTLS).
SendSecurity,
/// Deprecated option for backwards compatibility.
/// Deprecated option for backwards compatibilty.
///
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
SmtpCertificateChecks,
@@ -143,7 +143,7 @@ pub enum Config {
/// Send BCC copy to self.
///
/// Should be enabled for multidevice setups.
/// Default is 0 for chatmail accounts before a backup export, 1 otherwise.
#[strum(props(default = "1"))]
BccSelf,
/// True if encryption is preferred according to Autocrypt standard.
@@ -202,7 +202,7 @@ pub enum Config {
/// Value 1 is treated as "delete at once": messages are deleted
/// immediately, without moving to DeltaChat folder.
///
/// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise.
/// Default is 1 for chatmail accounts before a backup export, 0 otherwise.
DeleteServerAfter,
/// Timer in seconds after which the message is deleted from the
@@ -441,12 +441,6 @@ pub enum Config {
/// Enable webxdc realtime features.
#[strum(props(default = "1"))]
WebxdcRealtimeEnabled,
/// Last device token stored on the chatmail server.
///
/// If it has not changed, we do not store
/// the device token again.
DeviceToken,
}
impl Config {
@@ -519,19 +513,11 @@ impl Context {
// Default values
let val = match key {
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
false => Some("1"),
true => Some("0"),
},
Config::ConfiguredInboxFolder => Some("INBOX"),
Config::DeleteServerAfter => {
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
&& Box::pin(self.is_chatmail()).await?
{
true => Some("1"),
false => Some("0"),
}
}
Config::DeleteServerAfter => match Box::pin(self.is_chatmail()).await? {
false => Some("0"),
true => Some("1"),
},
_ => key.get_str("default"),
};
Ok(val.map(|s| s.to_string()))
@@ -1113,28 +1099,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_server_after_default() -> Result<()> {
let t = &TestContext::new_alice().await;
// Check that the settings are displayed correctly.
assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
assert_eq!(
t.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
// Leaving emails on the server even w/o `BccSelf` is a good default at least because other
// MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
// does).
t.set_config_bool(Config::BccSelf, false).await?;
assert_eq!(
t.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync() -> Result<()> {
let alice0 = TestContext::new_alice().await;

View File

@@ -61,7 +61,10 @@ macro_rules! progress {
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> Result<bool> {
self.sql.get_raw_config_bool("configured").await
self.sql
.get_raw_config_bool("configured")
.await
.map_err(Into::into)
}
/// Configures this account with the currently set parameters.
@@ -449,9 +452,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
let create = true;
imap_session
.select_with_uidvalidity(ctx, "INBOX", create)
.select_with_uidvalidity(ctx, "INBOX")
.await
.context("could not read INBOX status")?;

View File

@@ -1,7 +1,7 @@
//! Contacts module
use std::cmp::{min, Reverse};
use std::collections::{BinaryHeap, HashSet};
use std::collections::BinaryHeap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
@@ -34,6 +34,7 @@ use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::sql::{self, params_iter};
use crate::sync::{self, Sync::*};
use crate::tools::{duration_to_str, get_abs_path, smeared_time, time, SystemTime};
use crate::{chat, chatlist_events, stock_str};
@@ -113,8 +114,7 @@ impl ContactId {
SET gossiped_timestamp=0
WHERE EXISTS (SELECT 1 FROM chats_contacts
WHERE chats_contacts.chat_id=chats.id
AND chats_contacts.contact_id=?
AND chats_contacts.add_timestamp >= chats_contacts.remove_timestamp)",
AND chats_contacts.contact_id=?)",
(self,),
)
.await?;
@@ -129,19 +129,22 @@ impl ContactId {
) -> Result<()> {
context
.sql
.transaction(|transaction| {
let mut stmt = transaction
.prepare("UPDATE contacts SET origin=?1 WHERE id = ?2 AND origin < ?1")?;
for id in ids {
stmt.execute((origin, id))?;
}
Ok(())
})
.execute(
&format!(
"UPDATE contacts SET origin=? WHERE id IN ({}) AND origin<?",
sql::repeat_vars(ids.len())
),
rusqlite::params_from_iter(
params_iter(&[origin])
.chain(params_iter(ids))
.chain(params_iter(&[origin])),
),
)
.await?;
Ok(())
}
/// Returns contact address.
/// Returns contact adress.
pub async fn addr(&self, context: &Context) -> Result<String> {
let addr = context
.sql
@@ -1039,11 +1042,7 @@ impl Contact {
listflags: u32,
query: Option<&str>,
) -> Result<Vec<ContactId>> {
let self_addrs = context
.get_all_self_addrs()
.await?
.into_iter()
.collect::<HashSet<_>>();
let self_addrs = context.get_all_self_addrs().await?;
let mut add_self = false;
let mut ret = Vec::new();
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
@@ -1058,32 +1057,29 @@ impl Contact {
context
.sql
.query_map(
"SELECT c.id, c.addr FROM contacts c
&format!(
"SELECT c.id FROM contacts c \
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
WHERE c.id>?
WHERE c.addr NOT IN ({})
AND c.id>? \
AND c.origin>=? \
AND c.blocked=0 \
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
ORDER BY c.last_seen DESC, c.id DESC;",
(
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
minimal_origin,
&s3str_like_cmd,
&s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
),
|row| {
let id: ContactId = row.get(0)?;
let addr: String = row.get(1)?;
Ok((id, addr))
},
|rows| {
for row in rows {
let (id, addr) = row?;
if !self_addrs.contains(&addr) {
ret.push(id);
}
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 }
])),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
ret.push(id?);
}
Ok(())
},
@@ -1116,23 +1112,23 @@ impl Contact {
context
.sql
.query_map(
"SELECT id, addr FROM contacts
WHERE id>?
&format!(
"SELECT id FROM contacts
WHERE addr NOT IN ({})
AND id>?
AND origin>=?
AND blocked=0
ORDER BY last_seen DESC, id DESC;",
(ContactId::LAST_SPECIAL, minimal_origin),
|row| {
let id: ContactId = row.get(0)?;
let addr: String = row.get(1)?;
Ok((id, addr))
},
|rows| {
for row in rows {
let (id, addr) = row?;
if !self_addrs.contains(&addr) {
ret.push(id);
}
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(
params_iter(&self_addrs)
.chain(params_slice![ContactId::LAST_SPECIAL, minimal_origin]),
),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
ret.push(id?);
}
Ok(())
},
@@ -1988,7 +1984,7 @@ mod tests {
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
use crate::test_utils::{self, TestContext, TestContextManager};
#[test]
fn test_contact_id_values() {
@@ -2919,8 +2915,6 @@ Hi."#;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_was_seen_recently() -> Result<()> {
let _n = TimeShiftFalsePositiveNote;
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -2936,7 +2930,18 @@ Hi."#;
bob.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
assert!(contact.was_seen_recently());
let green = nu_ansi_term::Color::Green.normal();
assert!(
contact.was_seen_recently(),
"{}",
green.paint(
"\nNOTE: This test failure is probably a false-positive, caused by tests running in parallel.
The issue is that `SystemTime::shift()` (a utility function for tests) changes the time for all threads doing tests, and not only for the running test.
Until the false-positive is fixed:
- Use `cargo test -- --test-threads 1` instead of `cargo test`
- Or use `cargo nextest run` (install with `cargo install cargo-nextest --locked`)\n"
)
);
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
assert!(!self_contact.was_seen_recently());
@@ -3239,20 +3244,4 @@ Hi."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_is_verified() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
assert_eq!(contact.is_verified(&alice).await?, true);
assert!(contact.is_profile_verified(&alice).await?);
assert!(contact.get_verifier_id(&alice).await?.is_none());
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
Ok(())
}
}

View File

@@ -13,7 +13,7 @@ use async_channel::{self as channel, Receiver, Sender};
use pgp::types::PublicKeyTrait;
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use tokio::sync::{Mutex, Notify, OnceCell, RwLock};
use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
@@ -292,7 +292,7 @@ pub struct InnerContext {
pub(crate) push_subscribed: AtomicBool,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
pub(crate) iroh: OnceCell<Iroh>,
}
/// The state of ongoing process.
@@ -450,7 +450,7 @@ impl Context {
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
iroh: Arc::new(RwLock::new(None)),
iroh: OnceCell::new(),
};
let ctx = Context {
@@ -486,19 +486,6 @@ impl Context {
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
self.scheduler.stop(self).await;
if let Some(iroh) = self.iroh.write().await.take() {
// Close all QUIC connections.
// Spawn into a separate task,
// because Iroh calls `wait_idle()` internally
// and it may take time, especially if the network
// has become unavailable.
tokio::spawn(async move {
// We do not log the error because we do not want the task
// to hold the reference to Context.
let _ = tokio::time::timeout(Duration::from_secs(60), iroh.close()).await;
});
}
}
/// Restarts the IO scheduler if it was running before
@@ -509,7 +496,7 @@ impl Context {
/// Indicate that the network likely has come back.
pub async fn maybe_network(&self) {
if let Some(ref iroh) = *self.iroh.read().await {
if let Some(iroh) = self.iroh.get() {
iroh.network_change().await;
}
self.scheduler.maybe_network().await;
@@ -553,7 +540,23 @@ impl Context {
if self.scheduler.is_running().await {
self.scheduler.maybe_network().await;
self.wait_for_all_work_done().await;
// Wait until fetching is finished.
// Ideally we could wait for connectivity change events,
// but sleep loop is good enough.
// First 100 ms sleep in chunks of 10 ms.
for _ in 0..10 {
if self.all_work_done().await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
// If we are not finished in 100 ms, keep waking up every 100 ms.
while !self.all_work_done().await {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
} else {
// Pause the scheduler to ensure another connection does not start
// while we are fetching on a dedicated connection.
@@ -643,36 +646,14 @@ impl Context {
}
/// Emits a MsgsChanged event with specified chat and message ids
///
/// If IDs are unset, [`Self::emit_msgs_changed_without_ids`]
/// or [`Self::emit_msgs_changed_without_msg_id`] should be used
/// instead of this function.
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
debug_assert!(!chat_id.is_unset());
debug_assert!(!msg_id.is_unset());
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
chatlist_events::emit_chatlist_item_changed(self, chat_id);
}
/// Emits a MsgsChanged event with specified chat and without message id.
pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
debug_assert!(!chat_id.is_unset());
self.emit_event(EventType::MsgsChanged {
chat_id,
msg_id: MsgId::new(0),
});
chatlist_events::emit_chatlist_changed(self);
chatlist_events::emit_chatlist_item_changed(self, chat_id);
}
/// Emits an IncomingMsg event with specified chat and message ids
pub fn emit_incoming_msg(&self, chat_id: ChatId, msg_id: MsgId) {
debug_assert!(!chat_id.is_unset());
debug_assert!(!msg_id.is_unset());
self.emit_event(EventType::IncomingMsg { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
chatlist_events::emit_chatlist_item_changed(self, chat_id);
@@ -1777,7 +1758,6 @@ mod tests {
"socks5_password",
"key_id",
"webxdc_integration",
"device_token",
];
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();

View File

@@ -3,7 +3,7 @@
use std::cmp::max;
use std::collections::BTreeMap;
use anyhow::{anyhow, bail, ensure, Result};
use anyhow::{anyhow, bail, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
@@ -201,11 +201,7 @@ impl Session {
bail!("Attempt to fetch UID 0");
}
let create = false;
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
.await?;
ensure!(folder_exists, "No folder {folder}");
self.select_with_uidvalidity(context, folder).await?;
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);

View File

@@ -40,17 +40,20 @@ impl EncryptHelper {
/// Determines if we can and should encrypt.
///
/// For encryption to be enabled, `e2ee_guaranteed` should be true, or strictly more than a half
/// of peerstates should prefer encryption. Own preference is counted equally to peer
/// preferences, even if message copy is not sent to self.
///
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
///
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
pub(crate) async fn should_encrypt(
pub fn should_encrypt(
&self,
context: &Context,
e2ee_guaranteed: bool,
peerstates: &[(Option<Peerstate>, String)],
) -> Result<bool> {
let is_chatmail = context.is_chatmail().await?;
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
1
} else {
@@ -61,15 +64,10 @@ impl EncryptHelper {
Some(peerstate) => {
let prefer_encrypt = peerstate.prefer_encrypt;
info!(context, "Peerstate for {addr:?} is {prefer_encrypt}.");
if match peerstate.prefer_encrypt {
EncryptPreference::NoPreference | EncryptPreference::Reset => {
(peerstate.prefer_encrypt != EncryptPreference::Reset || is_chatmail)
&& self.prefer_encrypt == EncryptPreference::Mutual
}
EncryptPreference::Mutual => true,
} {
prefer_encrypt_count += 1;
}
match peerstate.prefer_encrypt {
EncryptPreference::NoPreference | EncryptPreference::Reset => {}
EncryptPreference::Mutual => prefer_encrypt_count += 1,
};
}
None => {
let msg = format!("Peerstate for {addr:?} missing, cannot encrypt");
@@ -172,11 +170,9 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::send_text_msg;
use crate::key::DcKey;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::receive_imf::receive_imf;
use crate::test_utils::{bob_keypair, TestContext, TestContextManager};
mod ensure_secret_key_exists {
@@ -324,109 +320,29 @@ Sent with my Delta Chat Messenger: https://delta.chat";
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_should_encrypt() -> Result<()> {
async fn test_should_encrypt() {
let t = TestContext::new_alice().await;
assert!(t.get_config_bool(Config::E2eeEnabled).await?);
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
// test with EncryptPreference::NoPreference:
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
let ps = new_peerstates(EncryptPreference::NoPreference);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
// Own preference is `Mutual` and we have the peer's key.
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with EncryptPreference::Reset
let ps = new_peerstates(EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with EncryptPreference::Mutual (self is also Mutual)
let ps = new_peerstates(EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with missing peerstate
let ps = vec![(None, "bob@foo.bar".to_string())];
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await.is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_should_encrypt_e2ee_disabled() -> Result<()> {
let t = &TestContext::new_alice().await;
t.set_config_bool(Config::E2eeEnabled, false).await?;
let encrypt_helper = EncryptHelper::new(t).await.unwrap();
let ps = new_peerstates(EncryptPreference::NoPreference);
assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
let ps = new_peerstates(EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(t, true, &ps).await?);
let mut ps = new_peerstates(EncryptPreference::Mutual);
// Own preference is `NoPreference` and there's no majority with `Mutual`.
assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
// Now the majority wants to encrypt. Let's encrypt, anyway there are other cases when we
// can't send unencrypted, e.g. protected groups.
ps.push(ps[0].clone());
assert!(encrypt_helper.should_encrypt(t, false, &ps).await?);
// Test with missing peerstate.
let ps = vec![(None, "bob@foo.bar".to_string())];
assert!(encrypt_helper.should_encrypt(t, true, &ps).await.is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chatmail_prefers_to_encrypt() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
bob.set_config_bool(Config::IsChatmail, true).await?;
let bob_chat_id = tcm
.send_recv_accept(alice, bob, "Hello from DC")
.await
.chat_id;
receive_imf(
bob,
b"From: alice@example.org\n\
To: bob@example.net\n\
Message-ID: <2222@example.org>\n\
Date: Sun, 22 Mar 3000 22:37:58 +0000\n\
\n\
Hello from another MUA\n",
false,
)
.await?;
send_text_msg(bob, bob_chat_id, "hi".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
let msg = Message::load_from_db(bob, sent_msg.sender_msg_id).await?;
assert!(msg.get_showpadlock());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
bob.set_config_bool(Config::IsChatmail, true).await?;
let bob_chat_id = receive_imf(
bob,
b"From: alice@example.org\n\
To: bob@example.net\n\
Message-ID: <2222@example.org>\n\
Date: Sun, 22 Mar 3000 22:37:58 +0000\n\
\n\
Hello\n",
false,
)
.await?
.unwrap()
.chat_id;
bob_chat_id.accept(bob).await?;
send_text_msg(bob, bob_chat_id, "hi".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
let msg = Message::load_from_db(bob, sent_msg.sender_msg_id).await?;
assert!(!msg.get_showpadlock());
Ok(())
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
}
}

View File

@@ -84,6 +84,7 @@ use crate::location;
use crate::log::LogExt;
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::sql::{self, params_iter};
use crate::stock_str;
use crate::tools::{duration_to_str, time, SystemTime};
@@ -328,44 +329,23 @@ pub(crate) async fn start_ephemeral_timers_msgids(
msg_ids: &[MsgId],
) -> Result<()> {
let now = time();
let should_interrupt =
context
.sql
.transaction(move |transaction| {
let mut should_interrupt = false;
let mut stmt =
transaction.prepare(
"UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer) AND ephemeral_timer > 0
AND id=?2")?;
for msg_id in msg_ids {
should_interrupt |= stmt.execute((now, msg_id))? > 0;
}
Ok(should_interrupt)
}).await?;
if should_interrupt {
context.scheduler.interrupt_ephemeral_task().await;
}
Ok(())
}
/// Starts ephemeral timer for all messages in the chat.
///
/// This should be called when chat is marked as noticed.
pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: ChatId) -> Result<()> {
let now = time();
let should_interrupt = context
let count = context
.sql
.execute(
"UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
WHERE chat_id = ?2
AND ephemeral_timer > 0
AND (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer)",
(now, chat_id),
&format!(
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
AND id IN ({})",
sql::repeat_vars(msg_ids.len())
),
rusqlite::params_from_iter(
std::iter::once(&now as &dyn crate::sql::ToSql)
.chain(std::iter::once(&now as &dyn crate::sql::ToSql))
.chain(params_iter(msg_ids)),
),
)
.await?
> 0;
if should_interrupt {
.await?;
if count > 0 {
context.scheduler.interrupt_ephemeral_task().await;
}
Ok(())
@@ -502,7 +482,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
}
for modified_chat_id in modified_chat_ids {
context.emit_msgs_changed_without_msg_id(modified_chat_id);
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
}
for msg_id in webxdc_deleted {
@@ -715,9 +695,7 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
use crate::config::Config;
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
use crate::download::DownloadState;
use crate::location;
use crate::message::markseen_msgs;
@@ -952,6 +930,7 @@ mod tests {
// Alice sends a text message.
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
@@ -978,12 +957,14 @@ mod tests {
// Alice sends message to Bob
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
// Alice sends second message to Bob, with no timer
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
@@ -1444,77 +1425,4 @@ mod tests {
Ok(())
}
/// Tests that ephemeral timer is started when the chat is noticed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_noticed_ephemeral_timer() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
let duration = 60;
chat.id
.set_ephemeral_timer(alice, Timer::Enabled { duration })
.await?;
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
marknoticed_chat(bob, bob_received_message.chat_id).await?;
SystemTime::shift(Duration::from_secs(100));
delete_expired_messages(bob, time()).await?;
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
.await?
.is_none());
Ok(())
}
/// Tests that archiving the chat starts ephemeral timer.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_archived_ephemeral_timer() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
let duration = 60;
chat.id
.set_ephemeral_timer(alice, Timer::Enabled { duration })
.await?;
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
bob_received_message
.chat_id
.set_visibility(bob, ChatVisibility::Archived)
.await?;
SystemTime::shift(Duration::from_secs(100));
delete_expired_messages(bob, time()).await?;
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
.await?
.is_none());
// Bob mutes the chat so it is not unarchived.
set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?;
// Now test that for already archived chat
// timer is started if all archived chats are marked as noticed.
let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await;
assert_eq!(bob_received_message_2.state, MessageState::InFresh);
marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
SystemTime::shift(Duration::from_secs(100));
delete_expired_messages(bob, time()).await?;
assert!(
Message::load_from_db_optional(bob, bob_received_message_2.id)
.await?
.is_none()
);
Ok(())
}
}

View File

@@ -59,7 +59,7 @@ pub enum EventType {
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a message box then.
/// in a messasge box then.
Error(String),
/// An action cannot be performed because the user is not in the group.
@@ -109,9 +109,6 @@ pub enum EventType {
/// A webxdc wants an info message or a changed summary to be notified.
IncomingWebxdcNotify {
/// ID of the chat.
chat_id: ChatId,
/// ID of the contact sending.
contact_id: ContactId,

View File

@@ -65,15 +65,6 @@ pub enum HeaderDef {
ChatGroupMemberAdded,
ChatContent,
/// Past members of the group.
ChatGroupPastMembers,
/// Space-separated timestamps of member addition
/// for members listed in the `To` field
/// followed by timestamps of member removal
/// for members listed in the `Chat-Group-Past-Members` field.
ChatGroupMemberTimestamps,
/// Duration of the attached media file.
ChatDuration,
@@ -82,7 +73,6 @@ pub enum HeaderDef {
/// [Autocrypt](https://autocrypt.org/) header.
Autocrypt,
AutocryptGossip,
AutocryptSetupMessage,
SecureJoin,

View File

@@ -7,8 +7,6 @@
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
use std::mem;
use anyhow::{Context as _, Result};
use base64::Engine as _;
use lettre_email::mime::Mime;
@@ -79,26 +77,21 @@ fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
struct HtmlMsgParser {
pub html: String,
pub plain: Option<PlainText>,
pub(crate) msg_html: String,
}
impl HtmlMsgParser {
/// Function takes a raw mime-message string,
/// searches for the main-text part
/// and returns that as parser.html
pub async fn from_bytes<'a>(
context: &Context,
rawmime: &'a [u8],
) -> Result<(Self, mailparse::ParsedMail<'a>)> {
pub async fn from_bytes(context: &Context, rawmime: &[u8]) -> Result<Self> {
let mut parser = HtmlMsgParser {
html: "".to_string(),
plain: None,
msg_html: "".to_string(),
};
let parsedmail = mailparse::parse_mail(rawmime).context("Failed to parse mail")?;
let parsedmail = mailparse::parse_mail(rawmime)?;
parser.collect_texts_recursive(context, &parsedmail).await?;
parser.collect_texts_recursive(&parsedmail).await?;
if parser.html.is_empty() {
if let Some(plain) = &parser.plain {
@@ -107,8 +100,8 @@ impl HtmlMsgParser {
} else {
parser.cid_to_data_recursive(context, &parsedmail).await?;
}
parser.html += &mem::take(&mut parser.msg_html);
Ok((parser, parsedmail))
Ok(parser)
}
/// Function iterates over all mime-parts
@@ -121,13 +114,12 @@ impl HtmlMsgParser {
/// therefore we use the first one.
async fn collect_texts_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
) -> Result<()> {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
Box::pin(self.collect_texts_recursive(context, cur_data)).await?
Box::pin(self.collect_texts_recursive(cur_data)).await?
}
Ok(())
}
@@ -136,35 +128,8 @@ impl HtmlMsgParser {
if raw.is_empty() {
return Ok(());
}
let (parser, mail) = Box::pin(HtmlMsgParser::from_bytes(context, &raw)).await?;
if !parser.html.is_empty() {
let mut text = "\r\n\r\n".to_string();
for h in mail.headers {
let key = h.get_key();
if matches!(
key.to_lowercase().as_str(),
"date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "subject"
) {
text += &format!("{key}: {}\r\n", h.get_value());
}
}
text += "\r\n";
self.msg_html += &PlainText {
text,
flowed: false,
delsp: false,
}
.to_html();
self.msg_html += &parser.html;
}
Ok(())
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
Box::pin(self.collect_texts_recursive(&mail)).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -210,7 +175,14 @@ impl HtmlMsgParser {
}
Ok(())
}
MimeMultipartType::Message => Ok(()),
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
Box::pin(self.cid_to_data_recursive(context, &mail)).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::IMAGE {
@@ -268,7 +240,7 @@ impl MsgId {
warn!(context, "get_html: parser error: {:#}", err);
Ok(None)
}
Ok((parser, _)) => Ok(Some(parser.html)),
Ok(parser) => Ok(Some(parser.html)),
}
} else {
warn!(context, "get_html: no mime for {}", self);
@@ -302,7 +274,7 @@ mod tests {
async fn test_htmlparse_plain_unspecified() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
@@ -320,7 +292,7 @@ This message does not have Content-Type nor Subject.<br/>
async fn test_htmlparse_plain_iso88591() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
@@ -338,7 +310,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
async fn test_htmlparse_plain_flowed() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert!(parser.plain.unwrap().flowed);
assert_eq!(
parser.html,
@@ -360,7 +332,7 @@ and will be wrapped as usual.<br/>
async fn test_htmlparse_alt_plain() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
@@ -381,7 +353,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_htmlparse_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_html.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
// on windows, `\r\n` linends are returned from mimeparser,
// however, rust multiline-strings use just `\n`;
@@ -399,7 +371,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_htmlparse_alt_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
r##"<html>
@@ -414,7 +386,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_htmlparse_alt_plain_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
r##"<html>
@@ -439,7 +411,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(test.find("data:").is_none());
// parsing converts cid: to data:
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert!(parser.html.contains("<html>"));
assert!(!parser.html.contains("Content-Id:"));
assert!(parser.html.contains("data:image/jpeg;base64,/9j/4AAQ"));

View File

@@ -13,7 +13,7 @@ use std::{
time::{Duration, UNIX_EPOCH},
};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use anyhow::{bail, format_err, Context as _, Result};
use async_channel::Receiver;
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
use deltachat_contact_tools::ContactAddress;
@@ -41,11 +41,11 @@ use crate::mimeparser;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::oauth2::get_oauth2_access_token;
use crate::push::encrypt_device_token;
use crate::receive_imf::{
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
};
use crate::scheduler::connectivity::ConnectivityStore;
use crate::sql;
use crate::stock_str;
use crate::tools::{self, create_id, duration_to_str};
@@ -407,7 +407,7 @@ impl Imap {
"IMAP-LOGIN as {}",
lp.user
)));
self.connectivity.set_preparing(context).await;
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
return Ok(session);
}
@@ -540,14 +540,10 @@ impl Imap {
return Ok(false);
}
let create = false;
let folder_exists = session
.select_with_uidvalidity(context, folder, create)
session
.select_with_uidvalidity(context, folder)
.await
.with_context(|| format!("Failed to select folder {folder:?}"))?;
if !folder_exists {
return Ok(false);
}
if !session.new_mail && !fetch_existing_msgs {
info!(context, "No new emails in folder {folder:?}.");
@@ -839,52 +835,45 @@ impl Session {
folder: &str,
folder_meaning: FolderMeaning,
) -> Result<()> {
let uid_validity;
// Collect pairs of UID and Message-ID.
let mut msgs = BTreeMap::new();
let create = false;
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
.await?;
if folder_exists {
let mut list = self
.uid_fetch("1:*", RFC724MID_UID)
.await
.with_context(|| format!("Can't resync folder {folder}"))?;
while let Some(fetch) = list.try_next().await? {
let headers = match get_fetch_headers(&fetch) {
Ok(headers) => headers,
Err(err) => {
warn!(context, "Failed to parse FETCH headers: {}", err);
continue;
}
};
let message_id = prefetch_get_message_id(&headers);
self.select_with_uidvalidity(context, folder).await?;
if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
msgs.insert(
uid,
(
rfc724_mid,
target_folder(context, folder, folder_meaning, &headers).await?,
),
);
let mut list = self
.uid_fetch("1:*", RFC724MID_UID)
.await
.with_context(|| format!("can't resync folder {folder}"))?;
while let Some(fetch) = list.try_next().await? {
let headers = match get_fetch_headers(&fetch) {
Ok(headers) => headers,
Err(err) => {
warn!(context, "Failed to parse FETCH headers: {}", err);
continue;
}
};
let message_id = prefetch_get_message_id(&headers);
if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
msgs.insert(
uid,
(
rfc724_mid,
target_folder(context, folder, folder_meaning, &headers).await?,
),
);
}
info!(
context,
"resync_folder_uids: Collected {} message IDs in {folder}.",
msgs.len(),
);
uid_validity = get_uidvalidity(context, folder).await?;
} else {
warn!(context, "resync_folder_uids: No folder {folder}.");
uid_validity = 0;
}
info!(
context,
"Resync: collected {} message IDs in folder {}",
msgs.len(),
folder,
);
let uid_validity = get_uidvalidity(context, folder).await?;
// Write collected UIDs to SQLite database.
context
.sql
@@ -921,15 +910,15 @@ impl Session {
.await?;
context
.sql
.transaction(|transaction| {
let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
for row_id in row_ids {
stmt.execute((row_id,))?;
}
Ok(())
})
.execute(
&format!(
"DELETE FROM imap WHERE id IN ({})",
sql::repeat_vars(row_ids.len())
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("Cannot remove deleted messages from imap table")?;
.context("cannot remove deleted messages from imap table")?;
context.emit_event(EventType::ImapMessageDeleted(format!(
"IMAP messages {uid_set} marked as deleted"
@@ -952,15 +941,15 @@ impl Session {
// Messages are moved or don't exist, IMAP returns OK response in both cases.
context
.sql
.transaction(|transaction| {
let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
for row_id in row_ids {
stmt.execute((row_id,))?;
}
Ok(())
})
.execute(
&format!(
"DELETE FROM imap WHERE id IN ({})",
sql::repeat_vars(row_ids.len())
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("Cannot delete moved messages from imap table")?;
.context("cannot delete moved messages from imap table")?;
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {set} moved to {target}"
)));
@@ -1006,15 +995,15 @@ impl Session {
}
context
.sql
.transaction(|transaction| {
let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
for row_id in row_ids {
stmt.execute((row_id,))?;
}
Ok(())
})
.execute(
&format!(
"UPDATE imap SET target='' WHERE id IN ({})",
sql::repeat_vars(row_ids.len())
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("Cannot plan deletion of messages")?;
.context("cannot plan deletion of messages")?;
if copy {
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {set} copied to {target}"
@@ -1050,11 +1039,7 @@ impl Session {
// MOVE/DELETE operations. This does not result in multiple SELECT commands
// being sent because `select_folder()` does nothing if the folder is already
// selected.
let create = false;
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
.await?;
ensure!(folder_exists, "No folder {folder}");
self.select_with_uidvalidity(context, folder).await?;
// Empty target folder name means messages should be deleted.
if target.is_empty() {
@@ -1148,40 +1133,29 @@ impl Session {
.await?;
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
let create = false;
let folder_exists = match self.select_with_uidvalidity(context, &folder, create).await {
Err(err) => {
warn!(
context,
"store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}.");
continue;
}
Ok(folder_exists) => folder_exists,
};
if !folder_exists {
warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
if let Err(err) = self.select_with_uidvalidity(context, &folder).await {
warn!(context, "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}.");
} else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
warn!(
context,
"Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}.");
continue;
} else {
info!(
context,
"Marked messages {} in folder {} as seen.", uid_set, folder
);
context
.sql
.execute(
&format!(
"DELETE FROM imap_markseen WHERE id IN ({})",
sql::repeat_vars(rowid_set.len())
),
rusqlite::params_from_iter(rowid_set),
)
.await
.context("cannot remove messages marked as seen from imap_markseen table")?;
}
context
.sql
.transaction(|transaction| {
let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
for rowid in rowid_set {
stmt.execute((rowid,))?;
}
Ok(())
})
.await
.context("Cannot remove messages marked as seen from imap_markseen table")?;
}
Ok(())
@@ -1197,14 +1171,9 @@ impl Session {
return Ok(());
}
let create = false;
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
self.select_with_uidvalidity(context, folder)
.await
.context("Failed to select folder")?;
if !folder_exists {
return Ok(());
}
.context("failed to select folder")?;
let mailbox = self
.selected_mailbox
@@ -1452,7 +1421,9 @@ impl Session {
let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
let rfc724_mid = if let Some(rfc724_mid) = uid_message_ids.get(&request_uid) {
rfc724_mid
} else {
error!(
context,
"No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
@@ -1589,52 +1560,16 @@ impl Session {
};
if self.can_metadata() && self.can_push() {
let device_token_changed =
context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
let folder = context
.get_config(Config::ConfiguredInboxFolder)
.await?
.context("INBOX is not configured")?;
if device_token_changed {
let folder = context
.get_config(Config::ConfiguredInboxFolder)
.await?
.context("INBOX is not configured")?;
let encrypted_device_token = encrypt_device_token(&device_token)
.context("Failed to encrypt device token")?;
// We expect that the server supporting `XDELTAPUSH` capability
// has non-synchronizing literals support as well:
// <https://www.rfc-editor.org/rfc/rfc7888>.
let encrypted_device_token_len = encrypted_device_token.len();
if encrypted_device_token_len <= 4096 {
self.run_command_and_check_ok(&format_setmetadata(
&folder,
&encrypted_device_token,
))
.await
.context("SETMETADATA command failed")?;
// Store device token saved on the server
// to prevent storing duplicate tokens.
// The server cannot deduplicate on its own
// because encryption gives a different
// result each time.
context
.set_config_internal(Config::DeviceToken, Some(&device_token))
.await?;
} else {
// If Apple or Google (FCM) gives us a very large token,
// do not even try to give it to IMAP servers.
//
// Limit of 4096 is arbitrarily selected
// to be the same as required by LITERAL- IMAP extension.
//
// Dovecot supports LITERAL+ and non-synchronizing literals
// of any length, but there is no reason for tokens
// to be that large even after OpenPGP encryption.
warn!(context, "Device token is too long for LITERAL-, ignoring.");
}
}
self.run_command_and_check_ok(format!(
"SETMETADATA \"{folder}\" (/private/devicetoken \"{device_token}\")"
))
.await
.context("SETMETADATA command failed")?;
context.push_subscribed.store(true, Ordering::Relaxed);
} else if !context.push_subscriber.heartbeat_subscribed().await {
let context = context.clone();
@@ -1646,17 +1581,10 @@ impl Session {
}
}
fn format_setmetadata(folder: &str, device_token: &str) -> String {
let device_token_len = device_token.len();
format!(
"SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
)
}
impl Session {
/// Returns success if we successfully set the flag or we otherwise
/// think add_flag should not be retried: Disconnection during setting
/// the flag, or other imap-errors, returns Ok as well.
/// the flag, or other imap-errors, returns true as well.
///
/// Returning error means that the operation can be retried.
async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
@@ -1703,11 +1631,7 @@ impl Session {
self.close().await?;
// Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
// emails moved before that wouldn't be fetched but considered "old" instead.
let create = false;
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
.await?;
ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
self.select_with_uidvalidity(context, folder).await?;
return Ok(Some(folder));
}
}
@@ -1718,10 +1642,7 @@ impl Session {
// Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
// the variants here.
for folder in folders {
match self
.select_with_uidvalidity(context, folder, create_mvbox)
.await
{
match self.select_with_uidvalidity(context, folder).await {
Ok(_) => {
info!(context, "MVBOX-folder {} created.", folder);
return Ok(Some(folder));
@@ -2572,14 +2493,10 @@ async fn add_all_recipients_as_contacts(
);
return Ok(());
};
let create = false;
let folder_exists = session
.select_with_uidvalidity(context, &mailbox, create)
session
.select_with_uidvalidity(context, &mailbox)
.await
.with_context(|| format!("could not select {mailbox}"))?;
if !folder_exists {
return Ok(());
}
let recipients = session
.get_all_recipients(context)
@@ -2947,16 +2864,4 @@ mod tests {
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
);
}
#[test]
fn test_setmetadata_device_token() {
assert_eq!(
format_setmetadata("INBOX", "foobarbaz"),
"SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)"
);
assert_eq!(
format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"),
"SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)"
);
}
}

View File

@@ -29,9 +29,7 @@ impl Session {
) -> Result<Self> {
use futures::future::FutureExt;
let create = true;
self.select_with_uidvalidity(context, folder, create)
.await?;
self.select_with_uidvalidity(context, folder).await?;
if self.drain_unsolicited_responses(context)? {
self.new_mail = true;

View File

@@ -34,7 +34,6 @@ impl Imap {
let watched_folders = get_watched_folders(context).await?;
let mut folder_configs = BTreeMap::new();
let mut folder_names = Vec::new();
for folder in folders {
let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
@@ -45,7 +44,6 @@ impl Imap {
// already been moved and left it in the inbox.
continue;
}
folder_names.push(folder.name().to_string());
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if let Some(config) = folder_meaning.to_config() {
@@ -93,7 +91,6 @@ impl Imap {
}
}
info!(context, "Found folders: {folder_names:?}.");
last_scan.replace(tools::Time::now());
Ok(true)
}

View File

@@ -111,8 +111,7 @@ impl ImapSession {
}
}
/// Selects a folder optionally creating it and takes care of UIDVALIDITY changes. Returns false
/// iff `folder` doesn't exist.
/// Selects a folder and takes care of UIDVALIDITY changes.
///
/// When selecting a folder for the first time, sets the uid_next to the current
/// mailbox.uid_next so that no old emails are fetched.
@@ -124,24 +123,11 @@ impl ImapSession {
&mut self,
context: &Context,
folder: &str,
create: bool,
) -> Result<bool> {
let newly_selected = if create {
self.select_or_create_folder(context, folder)
.await
.with_context(|| format!("failed to select or create folder {folder}"))?
} else {
match self.select_folder(context, folder).await {
Ok(newly_selected) => newly_selected,
Err(err) => match err {
Error::NoFolder(..) => return Ok(false),
_ => {
return Err(err)
.with_context(|| format!("failed to select folder {folder}"))?
}
},
}
};
) -> Result<()> {
let newly_selected = self
.select_or_create_folder(context, folder)
.await
.with_context(|| format!("failed to select or create folder {folder}"))?;
let mailbox = self
.selected_mailbox
.as_mut()
@@ -213,7 +199,7 @@ impl ImapSession {
}
}
return Ok(true);
return Ok(());
}
// UIDVALIDITY is modified, reset highest seen MODSEQ.
@@ -247,7 +233,7 @@ impl ImapSession {
old_uid_next,
old_uid_validity,
);
Ok(true)
Ok(())
}
}

View File

@@ -416,7 +416,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
.context("cannot import unpacked database");
}
if res.is_ok() {
res = adjust_bcc_self(context).await;
res = adjust_delete_server_after(context).await;
}
fs::remove_file(unpacked_database)
.await
@@ -796,7 +796,7 @@ async fn export_database(
.to_str()
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
adjust_bcc_self(context).await?;
adjust_delete_server_after(context).await?;
context
.sql
.set_raw_config_int("backup_time", timestamp)
@@ -826,14 +826,15 @@ async fn export_database(
.await
}
/// Sets `Config::BccSelf` (and `DeleteServerAfter` to "never" in effect) if needed so that new
/// messages are present on the server after a backup restoration or available for all devices in
/// multi-device case. NB: Calling this after a backup import isn't reliable as we can crash in
/// between, but this is a problem only for old backups, new backups already have `BccSelf` set if
/// necessary.
async fn adjust_bcc_self(context: &Context) -> Result<()> {
if context.is_chatmail().await? && !context.config_exists(Config::BccSelf).await? {
context.set_config(Config::BccSelf, Some("1")).await?;
/// Sets `Config::DeleteServerAfter` to "never" if needed so that new messages are present on the
/// server after a backup restoration or available for all devices in multi-device case.
/// NB: Calling this after a backup import isn't reliable as we can crash in between, but this is a
/// problem only for old backups, new backups already have `DeleteServerAfter` set if necessary.
async fn adjust_delete_server_after(context: &Context) -> Result<()> {
if context.is_chatmail().await? && !context.config_exists(Config::DeleteServerAfter).await? {
context
.set_config(Config::DeleteServerAfter, Some("0"))
.await?;
}
Ok(())
}
@@ -1029,20 +1030,12 @@ mod tests {
let context1 = &TestContext::new_alice().await;
// Check that the settings are displayed correctly.
assert_eq!(
context1.get_config(Config::BccSelf).await?,
Some("1".to_string())
);
// Check that the setting is displayed correctly.
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
context1.set_config_bool(Config::IsChatmail, true).await?;
assert_eq!(
context1.get_config(Config::BccSelf).await?,
Some("0".to_string())
);
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("1".to_string())
@@ -1065,10 +1058,6 @@ mod tests {
assert!(context2.is_configured().await?);
assert!(context2.is_chatmail().await?);
for ctx in [context1, context2] {
assert_eq!(
ctx.get_config(Config::BccSelf).await?,
Some("1".to_string())
);
assert_eq!(
ctx.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())

View File

@@ -178,7 +178,6 @@ impl BackupProvider {
}
info!(context, "Received valid backup authentication token.");
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
context.emit_event(EventType::ImexProgress(1));
let blobdir = BlobDirContents::new(&context).await?;
@@ -310,10 +309,6 @@ pub async fn get_backup2(
let mut file_size_buf = [0u8; 8];
recv_stream.read_exact(&mut file_size_buf).await?;
let file_size = u64::from_be_bytes(file_size_buf);
info!(context, "Received backup file size.");
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
context.emit_event(EventType::ImexProgress(1));
import_backup_stream(context, recv_stream, file_size, passphrase)
.await
.context("Failed to import backup from QUIC stream")?;
@@ -439,14 +434,12 @@ mod tests {
assert!(msg.save_file(&ctx1, &path).await.is_err());
// Check that both received the ImexProgress events.
for ctx in [&ctx0, &ctx1] {
ctx.evtracker
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1)))
.await;
ctx.evtracker
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
.await;
}
ctx0.evtracker
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
.await;
ctx1.evtracker
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -30,39 +30,7 @@ use crate::tools::{self, time_elapsed};
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
if let Ok(res) = res {
return Ok(res);
}
// Workaround for keys imported using
// Delta Chat core < 1.0.0.
// Old Delta Chat core had a bug
// that resulted in treating CRC24 checksum
// as part of the key when reading ASCII Armor.
// Some users that started using Delta Chat in 2019
// have such corrupted keys with garbage bytes at the end.
//
// Garbage is at least 3 bytes long
// and may be longer due to padding
// at the end of the real key data
// and importing the key multiple times.
//
// If removing 10 bytes is not enough,
// the key is likely actually corrupted.
for garbage_bytes in 3..std::cmp::min(bytes.len(), 10) {
let res = <Self as Deserializable>::from_bytes(Cursor::new(
bytes
.get(..bytes.len().saturating_sub(garbage_bytes))
.unwrap_or_default(),
));
if let Ok(res) = res {
return Ok(res);
}
}
// Removing garbage bytes did not help, return the error.
Ok(res?)
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
}
/// Create a key from a base64 string.
@@ -597,36 +565,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
}
/// Tests workaround for Delta Chat core < 1.0.0
/// which parsed CRC24 at the end of ASCII Armor
/// as the part of the key.
/// Depending on the alignment and the number of
/// `=` characters at the end of the key,
/// this resulted in various number of garbage
/// octets at the end of the key, starting from 3 octets,
/// but possibly 4 or 5 and maybe more octets
/// if the key is imported or transferred
/// using Autocrypt Setup Message multiple times.
#[test]
fn test_ignore_trailing_garbage() {
// Test several variants of garbage.
for garbage in [
b"\x02\xfc\xaa\x38\x4b\x5c".as_slice(),
b"\x02\xfc\xaa".as_slice(),
b"\x01\x02\x03\x04\x05".as_slice(),
] {
let private_key = KEYPAIR.secret.clone();
let mut binary = DcKey::to_bytes(&private_key);
binary.extend(garbage);
let private_key2 =
SignedSecretKey::from_slice(&binary).expect("Failed to ignore garbage");
assert_eq!(private_key.dc_fingerprint(), private_key2.dc_fingerprint());
}
}
#[test]
fn test_base64_roundtrip() {
let key = KEYPAIR.public.clone();

View File

@@ -707,6 +707,9 @@ pub(crate) async fn save(
))?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
drop(stmt_test);
drop(stmt_insert);
newest_timestamp = timestamp;
newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
}
@@ -1127,10 +1130,6 @@ Content-Disposition: attachment; filename="location.kml"
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
// Location-only messages are "auto-generated", but they mustn't make the contact a bot.
let contact = bob.add_or_lookup_contact(alice).await;
assert!(!contact.is_bot());
// Day later Bob removes location.
SystemTime::shift(Duration::from_secs(86400));
delete_expired(alice, time()).await?;

View File

@@ -293,7 +293,13 @@ impl MsgId {
ret += ", Location sent";
}
if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
let e2ee_errors = msg.param.get_int(Param::ErroneousE2ee).unwrap_or_default();
if 0 != e2ee_errors {
if 0 != e2ee_errors & 0x2 {
ret += ", Encrypted, no valid signature";
}
} else if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
ret += ", Encrypted";
}
@@ -342,7 +348,7 @@ impl MsgId {
let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
for server_url in server_urls {
// Format as RFC 5092 relative IMAP URL.
ret += &format!("\nServer-URL: {server_url}");
ret += &format!("\n{server_url}");
}
}
let hop_info = self.hop_info(context).await?;
@@ -718,7 +724,7 @@ impl Message {
/// `contact_id` set to [`ContactId::SELF`].
///
/// `latitude` is the North-south position of the location.
/// `longitude` is the East-west position of the location.
/// `longitutde` is the East-west position of the location.
///
/// [`location::set()`]: crate::location::set
/// [`send_locations_to_chat()`]: crate::location::send_locations_to_chat
@@ -947,6 +953,18 @@ impl Message {
cmd != SystemMessage::Unknown
}
/// Whether the message is still being created.
///
/// Messages with attachments might be created before the
/// attachment is ready. In this case some more restrictions on
/// the attachment apply, e.g. if the file to be attached is still
/// being written to or otherwise will still change it can not be
/// copied to the blobdir. Thus those attachments need to be
/// created immediately in the blobdir with a valid filename.
pub fn is_increation(&self) -> bool {
self.viewtype.has_file() && self.state == MessageState::OutPreparing
}
/// Returns true if the message is an Autocrypt Setup Message.
pub fn is_setupmessage(&self) -> bool {
if self.viewtype != Viewtype::File {
@@ -1111,9 +1129,7 @@ impl Message {
/// Updates message state from the vCard attachment.
pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
let vcard = fs::read(path)
.await
.with_context(|| format!("Could not read {path:?}"))?;
let vcard = fs::read(path).await.context("Could not read {path}")?;
if let Some(summary) = get_vcard_summary(&vcard) {
self.param.set(Param::Summary1, summary);
} else {
@@ -1609,15 +1625,15 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
modified_chat_ids.insert(msg.chat_id);
let target = context.get_delete_msgs_target().await?;
let update_db = |trans: &mut rusqlite::Transaction| {
trans.execute(
let update_db = |conn: &mut rusqlite::Connection| {
conn.execute(
"UPDATE imap SET target=? WHERE rfc724_mid=?",
(target, msg.rfc724_mid),
)?;
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
conn.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
Ok(())
};
if let Err(e) = context.sql.transaction(update_db).await {
if let Err(e) = context.sql.call_write(update_db).await {
error!(context, "delete_msgs: failed to update db: {e:#}.");
res = Err(e);
continue;
@@ -1639,7 +1655,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
res?;
for modified_chat_id in modified_chat_ids {
context.emit_msgs_changed_without_msg_id(modified_chat_id);
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
}
@@ -1669,12 +1685,12 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
.set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
.await?;
let mut msgs = Vec::with_capacity(msg_ids.len());
for &id in &msg_ids {
if let Some(msg) = context
.sql
.query_row_optional(
let msgs = context
.sql
.query_map(
&format!(
"SELECT
m.id AS id,
m.chat_id AS chat_id,
m.state AS state,
m.download_state as download_state,
@@ -1685,39 +1701,39 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
c.archived AS archived,
c.blocked AS blocked
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
WHERE m.id=? AND m.chat_id>9",
(id,),
|row| {
let chat_id: ChatId = row.get("chat_id")?;
let state: MessageState = row.get("state")?;
let download_state: DownloadState = row.get("download_state")?;
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let visibility: ChatVisibility = row.get("archived")?;
let blocked: Option<Blocked> = row.get("blocked")?;
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
Ok((
(
id,
chat_id,
state,
download_state,
param,
from_id,
rfc724_mid,
visibility,
blocked.unwrap_or_default(),
),
ephemeral_timer,
))
},
)
.await?
{
msgs.push(msg);
}
}
WHERE m.id IN ({}) AND m.chat_id>9",
sql::repeat_vars(msg_ids.len())
),
rusqlite::params_from_iter(&msg_ids),
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let state: MessageState = row.get("state")?;
let download_state: DownloadState = row.get("download_state")?;
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let visibility: ChatVisibility = row.get("archived")?;
let blocked: Option<Blocked> = row.get("blocked")?;
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
Ok((
(
id,
chat_id,
state,
download_state,
param,
from_id,
rfc724_mid,
visibility,
blocked.unwrap_or_default(),
),
ephemeral_timer,
))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
if msgs
.iter()
@@ -2090,9 +2106,6 @@ pub enum Viewtype {
Gif = 21,
/// Message containing a sticker, similar to image.
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
///
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker = 23,
@@ -2193,6 +2206,38 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prepare_message_and_send() {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"))
.await
.unwrap();
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
let msg_id = chat::prepare_msg(ctx, chat.id, &mut msg).await.unwrap();
let _msg2 = Message::load_from_db(ctx, msg_id).await.unwrap();
assert_eq!(_msg2.get_filemime(), None);
}
/// Tests that message can be prepared even if account has no configured address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prepare_not_configured() {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_webrtc_instance() {
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
@@ -2312,9 +2357,9 @@ mod tests {
let mut msg = Message::new_text("Quoted message".to_string());
// Send message, so it gets a Message-Id.
// Prepare message for sending, so it gets a Message-Id.
assert!(msg.rfc724_mid.is_empty());
let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap();
let msg_id = chat::prepare_msg(ctx, chat.id, &mut msg).await.unwrap();
let msg = Message::load_from_db(ctx, msg_id).await.unwrap();
assert!(!msg.rfc724_mid.is_empty());
@@ -2709,29 +2754,6 @@ mod tests {
async fn test_is_bot() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice receives an auto-generated non-chat message.
//
// This could be a holiday notice,
// in which case the message should be marked as bot-generated,
// but the contact should not.
receive_imf(
&alice,
b"From: Claire <claire@example.com>\n\
To: alice@example.org\n\
Message-ID: <789@example.com>\n\
Auto-Submitted: auto-generated\n\
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello\n",
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text(), "hello".to_string());
assert!(msg.is_bot());
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
assert!(!contact.is_bot());
// Alice receives a message from Bob the bot.
receive_imf(
&alice,

View File

@@ -66,36 +66,8 @@ pub struct MimeFactory {
selfstatus: String,
/// Vector of actual recipient addresses.
///
/// This is the list of addresses the message should be sent to.
/// It is not the same as the `To` header,
/// because in case of "member removed" message
/// removed member is in the recipient list,
/// but not in the `To` header.
/// In case of broadcast lists there are multiple recipients,
/// but the `To` header has no members.
///
/// If `bcc_self` configuration is enabled,
/// this list will be extended with own address later,
/// but `MimeFactory` is not responsible for this.
recipients: Vec<String>,
/// Vector of pairs of recipient name and address that goes into the `To` field.
///
/// The list of actual message recipient addresses may be different,
/// e.g. if members are hidden for broadcast lists.
to: Vec<(String, String)>,
/// Vector of pairs of past group member names and addresses.
past_members: Vec<(String, String)>,
/// Timestamps of the members in the same order as in the `recipients`
/// followed by `past_members`.
///
/// If this is not empty, its length
/// should be the sum of `recipients` and `past_members` length.
member_timestamps: Vec<i64>,
/// Vector of pairs of recipient name and address
recipients: Vec<(String, String)>,
timestamp: i64,
loaded: Loaded,
@@ -156,7 +128,6 @@ impl MimeFactory {
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
let chat = Chat::load_from_db(context, msg.chat_id).await?;
let attach_profile_data = Self::should_attach_profile_data(&msg);
let undisclosed_recipients = chat.typ == Chattype::Broadcast;
let from_addr = context.get_primary_self_addr().await?;
let config_displayname = context
@@ -174,101 +145,47 @@ impl MimeFactory {
(name, None)
};
let mut recipients = Vec::new();
let mut to = Vec::new();
let mut past_members = Vec::new();
let mut member_timestamps = Vec::new();
let mut recipients = Vec::with_capacity(5);
let mut recipient_ids = HashSet::new();
let mut req_mdn = false;
if chat.is_self_talk() {
if msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
recipients.push(from_addr.to_string());
to.push((from_displayname.to_string(), from_addr.to_string()));
recipients.push((from_displayname.to_string(), from_addr.to_string()));
}
} else if chat.is_mailing_list() {
let list_post = chat
.param
.get(Param::ListPost)
.context("Can't write to mailinglist without ListPost param")?;
to.push(("".to_string(), list_post.to_string()));
recipients.push(list_post.to_string());
recipients.push(("".to_string(), list_post.to_string()));
} else {
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
msg.param.get(Param::Arg)
} else {
None
};
context
.sql
.query_map(
"SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp
FROM chats_contacts cc
LEFT JOIN contacts c ON cc.contact_id=c.id
WHERE cc.chat_id=? AND cc.contact_id>9 OR (cc.contact_id=1 AND ?)",
(msg.chat_id, chat.typ == Chattype::Group),
"SELECT c.authname, c.addr, c.id \
FROM chats_contacts cc \
LEFT JOIN contacts c ON cc.contact_id=c.id \
WHERE cc.chat_id=? AND cc.contact_id>9;",
(msg.chat_id,),
|row| {
let authname: String = row.get(0)?;
let addr: String = row.get(1)?;
let id: ContactId = row.get(2)?;
let add_timestamp: i64 = row.get(3)?;
let remove_timestamp: i64 = row.get(4)?;
Ok((authname, addr, id, add_timestamp, remove_timestamp))
Ok((authname, addr, id))
},
|rows| {
let mut past_member_timestamps = Vec::new();
for row in rows {
let (authname, addr, id, add_timestamp, remove_timestamp) = row?;
let addr = if id == ContactId::SELF {
from_addr.to_string()
} else {
addr
};
let name = match attach_profile_data {
true => authname,
false => "".to_string(),
};
if add_timestamp >= remove_timestamp {
if !recipients_contain_addr(&to, &addr) {
recipients.push(addr.clone());
if !undisclosed_recipients {
to.push((name, addr));
member_timestamps.push(add_timestamp);
}
}
recipient_ids.insert(id);
} else {
// Row is a tombstone,
// member is not actually part of the group.
if !recipients_contain_addr(&past_members, &addr) {
if let Some(email_to_remove) = email_to_remove {
if email_to_remove == addr {
// This is a "member removed" message,
// we need to notify removed member
// that it was removed.
recipients.push(addr.clone());
}
}
if !undisclosed_recipients {
past_members.push((name, addr));
past_member_timestamps.push(remove_timestamp);
}
}
let (authname, addr, id) = row?;
if !recipients_contain_addr(&recipients, &addr) {
let name = match attach_profile_data {
true => authname,
false => "".to_string(),
};
recipients.push((name, addr));
}
recipient_ids.insert(id);
}
debug_assert!(member_timestamps.len() >= to.len());
if to.len() > 1 {
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
to.remove(position);
member_timestamps.remove(position);
}
}
member_timestamps.extend(past_member_timestamps);
Ok(())
},
)
@@ -309,19 +226,12 @@ impl MimeFactory {
};
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await;
debug_assert!(
member_timestamps.is_empty()
|| to.len() + past_members.len() == member_timestamps.len()
);
let factory = MimeFactory {
from_addr,
from_displayname,
sender_displayname,
selfstatus,
recipients,
to,
past_members,
member_timestamps,
timestamp: msg.timestamp_sort,
loaded: Loaded::Message { msg, chat },
in_reply_to,
@@ -349,10 +259,7 @@ impl MimeFactory {
from_displayname: "".to_string(),
sender_displayname: None,
selfstatus: "".to_string(),
recipients: vec![contact.get_addr().to_string()],
to: vec![("".to_string(), contact.get_addr().to_string())],
past_members: vec![],
member_timestamps: vec![],
recipients: vec![("".to_string(), contact.get_addr().to_string())],
timestamp,
loaded: Loaded::Mdn {
rfc724_mid,
@@ -376,7 +283,11 @@ impl MimeFactory {
let self_addr = context.get_primary_self_addr().await?;
let mut res = Vec::new();
for addr in self.recipients.iter().filter(|&addr| *addr != self_addr) {
for (_, addr) in self
.recipients
.iter()
.filter(|(_, addr)| addr != &self_addr)
{
res.push((Peerstate::from_addr(context, addr).await?, addr.clone()));
}
@@ -445,7 +356,7 @@ impl MimeFactory {
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
// Otherwise "smeared timestamps" may result in the condition
// Othewise "smeared timestamps" may result in the condition
// to fail even if the clock is monotonic.
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
Ok(true)
@@ -564,7 +475,10 @@ impl MimeFactory {
}
pub fn recipients(&self) -> Vec<String> {
self.recipients.clone()
self.recipients
.iter()
.map(|(_, addr)| addr.clone())
.collect()
}
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
@@ -574,33 +488,46 @@ impl MimeFactory {
let from = new_address_with_name(&self.from_displayname, self.from_addr.clone());
let undisclosed_recipients = match &self.loaded {
Loaded::Message { chat, .. } => chat.typ == Chattype::Broadcast,
Loaded::Mdn { .. } => false,
};
let mut to = Vec::new();
for (name, addr) in &self.to {
if name.is_empty() {
to.push(Address::new_mailbox(addr.clone()));
} else {
to.push(new_address_with_name(name, addr.clone()));
}
}
let mut past_members = Vec::new(); // Contents of `Chat-Group-Past-Members` header.
for (name, addr) in &self.past_members {
if name.is_empty() {
past_members.push(Address::new_mailbox(addr.clone()));
} else {
past_members.push(new_address_with_name(name, addr.clone()));
}
}
debug_assert!(
self.member_timestamps.is_empty()
|| to.len() + past_members.len() == self.member_timestamps.len()
);
if to.is_empty() {
if undisclosed_recipients {
to.push(Address::new_group(
"hidden-recipients".to_string(),
Vec::new(),
));
} else {
let email_to_remove = match &self.loaded {
Loaded::Message { msg, .. } => {
if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
msg.param.get(Param::Arg)
} else {
None
}
}
Loaded::Mdn { .. } => None,
};
for (name, addr) in &self.recipients {
if let Some(email_to_remove) = email_to_remove {
if email_to_remove == addr {
continue;
}
}
if name.is_empty() {
to.push(Address::new_mailbox(addr.clone()));
} else {
to.push(new_address_with_name(name, addr.clone()));
}
}
if to.is_empty() {
to.push(from.clone());
}
}
// Start with Internet Message Format headers in the order of the standard example
@@ -613,26 +540,6 @@ impl MimeFactory {
headers.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
}
headers.push(Header::new_with_value("To".into(), to.clone()).unwrap());
if !past_members.is_empty() {
headers.push(
Header::new_with_value("Chat-Group-Past-Members".into(), past_members.clone())
.unwrap(),
);
}
if !self.member_timestamps.is_empty() {
headers.push(
Header::new_with_value(
"Chat-Group-Member-Timestamps".into(),
self.member_timestamps
.iter()
.map(|ts| ts.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.unwrap(),
);
}
let subject_str = self.subject_str(context).await?;
let encoded_subject = if subject_str
@@ -745,9 +652,7 @@ impl MimeFactory {
let peerstates = self.peerstates_for_recipients(context).await?;
let is_encrypted = !self.should_force_plaintext()
&& encrypt_helper
.should_encrypt(context, e2ee_guaranteed, &peerstates)
.await?;
&& encrypt_helper.should_encrypt(context, e2ee_guaranteed, &peerstates)?;
let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
} else {
@@ -1464,7 +1369,7 @@ impl MimeFactory {
// add attachment part
if msg.viewtype.has_file() {
let file_part = build_body_file(context, &msg).await?;
let (file_part, _) = build_body_file(context, &msg, "").await?;
parts.push(file_part);
}
@@ -1604,10 +1509,14 @@ pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
.join("\r\n")
}
async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder> {
async fn build_body_file(
context: &Context,
msg: &Message,
base_name: &str,
) -> Result<(PartBuilder, String)> {
let blob = msg
.param
.get_blob(Param::File, context)
.get_blob(Param::File, context, true)
.await?
.context("msg has no file")?;
let suffix = blob.suffix().unwrap_or("dat");
@@ -1630,13 +1539,17 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder
),
Viewtype::Image | Viewtype::Gif => format!(
"image_{}.{}",
chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0)
.single()
.map_or_else(
|| "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
),
if base_name.is_empty() {
chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0)
.single()
.map_or_else(
|| "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
)
} else {
base_name.to_string()
},
&suffix,
),
Viewtype::Video => format!(
@@ -1688,7 +1601,7 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder
.header(("Content-Transfer-Encoding", "base64"))
.body(encoded_body);
Ok(mail)
Ok((mail, filename_to_send))
}
async fn build_avatar_file(context: &Context, path: &str) -> Result<String> {
@@ -1992,7 +1905,7 @@ mod tests {
)
.await
.unwrap();
let mut new_msg = incoming_msg_to_reply_msg(
let new_msg = incoming_msg_to_reply_msg(
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: bob@example.com\n\
To: alice@example.org\n\
@@ -2018,9 +1931,6 @@ mod tests {
Original-Message-ID: <2893@example.com>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n", &t).await;
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
.await
.unwrap();
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
// The subject string should not be "Re: message opened"
assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap());
@@ -2167,7 +2077,7 @@ mod tests {
let mut new_msg = Message::new_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap();
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
@@ -2224,7 +2134,7 @@ mod tests {
) -> String {
let t = TestContext::new_alice().await;
let mut new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await;
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 1).await;
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 2).await;
if delete_original_msg {
incoming_msg.id.trash(&t, false).await.unwrap();
@@ -2254,9 +2164,6 @@ mod tests {
new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap();
}
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
.await
.unwrap();
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
mf.subject_str(&t).await.unwrap()
}
@@ -2277,6 +2184,9 @@ mod tests {
let mut new_msg = Message::new_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::prepare_msg(context, chat_id, &mut new_msg)
.await
.unwrap();
new_msg
}
@@ -2287,7 +2197,7 @@ mod tests {
let t = TestContext::new_alice().await;
let context = &t;
let mut msg = incoming_msg_to_reply_msg(
let msg = incoming_msg_to_reply_msg(
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: Charlie <charlie@example.com>\n\
To: alice@example.org\n\
@@ -2300,7 +2210,6 @@ mod tests {
context,
)
.await;
chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap();
let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap();
@@ -2554,9 +2463,8 @@ mod tests {
// Alice creates a group with Bob and Claire and then removes Bob.
let alice = TestContext::new_alice().await;
let claire_addr = "claire@foo.de";
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "Claire", claire_addr).await?;
let claire_id = Contact::create(&alice, "Claire", "claire@foo.de").await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
@@ -2572,17 +2480,10 @@ mod tests {
.get_first_header("To")
.context("no To: header parsed")?;
let to = addrparse_header(to)?;
for to_addr in to.iter() {
match to_addr {
mailparse::MailAddr::Single(ref info) => {
// Addresses should be of existing members (Alice and Bob) and not Claire.
assert_ne!(info.addr, claire_addr);
}
mailparse::MailAddr::Group(_) => {
panic!("Group addresses are not expected here");
}
}
}
let mailbox = to
.extract_single_info()
.context("to: field does not contain exactly one address")?;
assert_eq!(mailbox.addr, "bob@example.net");
Ok(())
}

View File

@@ -17,10 +17,10 @@ use rand::distributions::{Alphanumeric, DistString};
use crate::aheader::{Aheader, EncryptPreference};
use crate::authres::handle_authres;
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::chat::{add_info_msg, ChatId};
use crate::config::Config;
use crate::constants;
use crate::contact::ContactId;
use crate::constants::{self, Chattype};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::decrypt::{
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
@@ -35,9 +35,9 @@ use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
use crate::sync::SyncItems;
use crate::tools::time;
use crate::tools::{
get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text, validate_id,
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text,
validate_id,
};
use crate::{chatlist_events, location, stock_str, tools};
@@ -58,14 +58,9 @@ pub(crate) struct MimeMessage {
/// Message headers.
headers: HashMap<String, String>,
/// List of addresses from the `To` and `Cc` headers.
///
/// Addresses are normalized and lowercase.
/// Addresses are normalized and lowercase
pub recipients: Vec<SingleInfo>,
/// List of addresses from the `Chat-Group-Past-Members` header.
pub past_members: Vec<SingleInfo>,
/// `From:` address.
pub from: SingleInfo,
@@ -111,26 +106,20 @@ pub(crate) struct MimeMessage {
/// received.
pub(crate) footer: Option<String>,
/// If set, this is a modified MIME message; clients should offer a way to view the original
/// MIME message in this case.
// if this flag is set, the parts/text/etc. are just close to the original mime-message;
// clients should offer a way to view the original message in this case
pub is_mime_modified: bool,
/// Decrypted, raw MIME structure. Nonempty iff `is_mime_modified` and the message was actually
/// encrypted.
/// The decrypted, raw mime structure.
///
/// This is non-empty iff `is_mime_modified` and the message was actually encrypted. It is used
/// for e.g. late-parsing HTML.
pub decoded_data: Vec<u8>,
/// Hop info for debugging.
pub(crate) hop_info: String,
/// Whether the message is auto-generated.
///
/// If chat message (with `Chat-Version` header) is auto-generated,
/// the contact sending this should be marked as bot.
///
/// If non-chat message is auto-generated,
/// it could be a holiday notice auto-reply,
/// in which case the message should be marked as bot-generated,
/// but the contact should not be.
/// Whether the contact sending this should be marked as bot or non-bot.
pub(crate) is_bot: Option<bool>,
/// When the message was received, in secs since epoch.
@@ -238,7 +227,6 @@ impl MimeMessage {
let mut headers = Default::default();
let mut recipients = Default::default();
let mut past_members = Default::default();
let mut from = Default::default();
let mut list_post = Default::default();
let mut chat_disposition_notification_to = None;
@@ -248,7 +236,6 @@ impl MimeMessage {
context,
&mut headers,
&mut recipients,
&mut past_members,
&mut from,
&mut list_post,
&mut chat_disposition_notification_to,
@@ -269,7 +256,6 @@ impl MimeMessage {
context,
&mut headers,
&mut recipients,
&mut past_members,
&mut from,
&mut list_post,
&mut chat_disposition_notification_to,
@@ -447,8 +433,6 @@ impl MimeMessage {
HeaderDef::ChatGroupAvatar,
HeaderDef::ChatGroupMemberRemoved,
HeaderDef::ChatGroupMemberAdded,
HeaderDef::ChatGroupMemberTimestamps,
HeaderDef::ChatGroupPastMembers,
] {
headers.remove(h.get_headername());
}
@@ -465,7 +449,6 @@ impl MimeMessage {
context,
&mut headers,
&mut recipients,
&mut past_members,
&mut inner_from,
&mut list_post,
&mut chat_disposition_notification_to,
@@ -523,7 +506,6 @@ impl MimeMessage {
parts: Vec::new(),
headers,
recipients,
past_members,
list_post,
from,
from_is_signed,
@@ -583,14 +565,11 @@ impl MimeMessage {
},
};
let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
if parser.mdn_reports.is_empty()
&& !is_location_only
&& parser.sync_items.is_none()
&& parser.webxdc_status_update.is_none()
{
let is_bot =
parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
if parser.mdn_reports.is_empty() && parser.webxdc_status_update.is_none() {
// "Auto-Submitted" is also set by holiday-notices so we also check "chat-version".
let is_bot = parser.headers.get("auto-submitted")
== Some(&"auto-generated".to_string())
&& parser.headers.contains_key("chat-version");
parser.is_bot = Some(is_bot);
}
parser.maybe_remove_bad_parts();
@@ -692,7 +671,7 @@ impl MimeMessage {
&& self
.parts
.get(1)
.is_some_and(|filepart| match filepart.typ {
.map_or(false, |filepart| match filepart.typ {
Viewtype::Image
| Viewtype::Gif
| Viewtype::Sticker
@@ -1543,12 +1522,10 @@ impl MimeMessage {
}
}
#[allow(clippy::too_many_arguments)]
fn merge_headers(
context: &Context,
headers: &mut HashMap<String, String>,
recipients: &mut Vec<SingleInfo>,
past_members: &mut Vec<SingleInfo>,
from: &mut Option<SingleInfo>,
list_post: &mut Option<String>,
chat_disposition_notification_to: &mut Option<SingleInfo>,
@@ -1577,11 +1554,6 @@ impl MimeMessage {
if !recipients_new.is_empty() {
*recipients = recipients_new;
}
let past_members_addresses =
get_all_addresses_from_header(fields, "chat-group-past-members");
if !past_members_addresses.is_empty() {
*past_members = past_members_addresses;
}
let from_new = get_from(fields);
if from_new.is_some() {
*from = from_new;
@@ -1693,8 +1665,18 @@ impl MimeMessage {
.get_header_value(HeaderDef::MessageId)
.and_then(|v| parse_message_id(&v).ok())
{
let mut to_list =
get_all_addresses_from_header(&report.headers, "x-failed-recipients");
let to = if to_list.len() != 1 {
// We do not know which recipient failed
None
} else {
to_list.pop()
};
return Ok(Some(DeliveryReport {
rfc724_mid: original_message_id,
failed_recipient: to.map(|s| s.addr),
failure,
}));
}
@@ -1792,6 +1774,7 @@ impl MimeMessage {
{
self.delivery_report = Some(DeliveryReport {
rfc724_mid: original_message_id,
failed_recipient: None,
failure: true,
})
}
@@ -1848,20 +1831,6 @@ impl MimeMessage {
};
Ok(parent_timestamp)
}
/// Returns parsed `Chat-Group-Member-Timestamps` header contents.
///
/// Returns `None` if there is no such header.
pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
self.get_header(HeaderDef::ChatGroupMemberTimestamps)
.map(|h| {
h.split_ascii_whitespace()
.filter_map(|ts| ts.parse::<i64>().ok())
.map(|ts| std::cmp::min(now, ts))
.collect()
})
}
}
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
@@ -1943,6 +1912,7 @@ pub(crate) struct Report {
#[derive(Debug)]
pub(crate) struct DeliveryReport {
pub rfc724_mid: String,
pub failed_recipient: Option<String>,
pub failure: bool,
}
@@ -2308,12 +2278,20 @@ async fn handle_ndn(
let msgs: Vec<_> = context
.sql
.query_map(
"SELECT id FROM msgs
WHERE rfc724_mid=? AND from_id=1",
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" c.type AS type",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
),
(&failed.rfc724_mid,),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
let msg_id: MsgId = row.get("msg_id")?;
let chat_id: ChatId = row.get("chat_id")?;
let chat_type: Chattype = row.get("type")?;
Ok((msg_id, chat_id, chat_type))
},
|rows| Ok(rows.collect::<Vec<_>>()),
)
@@ -2321,13 +2299,16 @@ async fn handle_ndn(
let error = if let Some(error) = error {
error
} else if let Some(failed_recipient) = &failed.failed_recipient {
format!("Delivery to {failed_recipient} failed.").clone()
} else {
"Delivery to at least one recipient failed.".to_string()
};
let err_msg = &error;
let mut first = true;
for msg in msgs {
let msg_id = msg?;
let (msg_id, chat_id, chat_type) = msg?;
let mut message = Message::load_from_db(context, msg_id).await?;
let aggregated_error = message
.error
@@ -2339,11 +2320,47 @@ async fn handle_ndn(
aggregated_error.as_ref().unwrap_or(err_msg),
)
.await?;
if first {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
}
first = false;
}
Ok(())
}
async fn ndn_maybe_add_info_msg(
context: &Context,
failed: &DeliveryReport,
chat_id: ChatId,
chat_type: Chattype,
) -> Result<()> {
match chat_type {
Chattype::Group | Chattype::Broadcast => {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
.await?
.context("contact ID not found")?;
let contact = Contact::get_by_id(context, contact_id).await?;
// Tell the user which of the recipients failed if we know that (because in
// a group, this might otherwise be unclear)
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
context.emit_event(EventType::ChatModified(chat_id));
}
}
Chattype::Mailinglist => {
// ndn_maybe_add_info_msg() is about the case when delivery to the group failed.
// If we get an NDN for the mailing list, just issue a warning.
warn!(context, "ignoring NDN for mailing list.");
}
Chattype::Single => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use mailparse::ParsedMail;
@@ -3134,7 +3151,11 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
// Make sure the file is there even though the html is wrong:
let param = &message.parts[0].param;
let blob: BlobObject = param.get_blob(Param::File, &t).await.unwrap().unwrap();
let blob: BlobObject = param
.get_blob(Param::File, &t, false)
.await
.unwrap()
.unwrap();
let f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap();
let size = f.metadata().await.unwrap().len();
assert_eq!(size, 154);
@@ -3632,10 +3653,9 @@ On 2020-10-25, Bob wrote:
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mime_modified_large_plain() -> Result<()> {
let t = TestContext::new_alice().await;
let t1 = TestContext::new_alice().await;
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
static REPEAT_CNT: usize = DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + 2;
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
@@ -3656,21 +3676,22 @@ On 2020-10-25, Bob wrote:
if draft {
chat.id.set_draft(&t, Some(&mut msg)).await?;
}
let sent_msg = t.send_msg(chat.id, &mut msg).await;
t.send_msg(chat.id, &mut msg).await;
let msg = t.get_last_msg_in(chat.id).await;
assert!(msg.has_html());
let html = msg.id.get_html(&t).await?.unwrap();
assert_eq!(html.matches("<!DOCTYPE html>").count(), 1);
assert_eq!(html.matches("just repeated.<br/>").count(), REPEAT_CNT);
assert_eq!(
msg.id
.get_html(&t)
.await?
.unwrap()
.matches("just repeated")
.count(),
REPEAT_CNT
);
assert!(
msg.text.matches("just repeated.").count()
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
msg.text.matches("just repeated").count() <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
assert!(msg.text.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
let msg = t1.recv_msg(&sent_msg).await;
assert!(msg.has_html());
assert_eq!(msg.id.get_html(&t1).await?.unwrap(), html);
}
t.set_config(Config::Bot, Some("1")).await?;

View File

@@ -47,7 +47,7 @@ pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
Ok(())
}
/// Update the timestamp of the last successful connection
/// Update the timestamp of the last successfull connection
/// to the given `host` and `port`
/// with the given application protocol `alpn`.
///

View File

@@ -6,17 +6,14 @@ use http_body_util::BodyExt;
use hyper_util::rt::TokioIo;
use mime::Mime;
use serde::Serialize;
use tokio::fs;
use crate::blob::BlobObject;
use crate::context::Context;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_rustls;
use crate::tools::{create_id, time};
/// HTTP(S) GET response.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub struct Response {
/// Response body.
pub blob: Vec<u8>,
@@ -93,146 +90,9 @@ where
Ok(sender)
}
/// Converts the URL to expiration and stale timestamps.
fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
let now = time();
let expires = now + 3600 * 24 * 35;
let stale = if url.ends_with(".xdc") {
// WebXDCs are never stale, they just expire.
expires
} else if mimetype.is_some_and(|s| s.starts_with("image/")) {
// Cache images for 1 day.
//
// As of 2024-12-12 WebXDC icons at <https://webxdc.org/apps/>
// use the same path for all app versions,
// so may change, but it is not critical if outdated icon is displayed.
now + 3600 * 24
} else {
// Revalidate everything else after 1 hour.
//
// This includes HTML, CSS and JS.
now + 3600
};
(expires, stale)
}
/// Places the binary into HTTP cache.
async fn http_cache_put(context: &Context, url: &str, response: &Response) -> Result<()> {
let blob = BlobObject::create(
context,
&format!("http_cache_{}", create_id()),
response.blob.as_slice(),
)
.await?;
let (expires, stale) = http_url_cache_timestamps(url, response.mimetype.as_deref());
context
.sql
.insert(
"INSERT OR REPLACE INTO http_cache (url, expires, stale, blobname, mimetype, encoding)
VALUES (?, ?, ?, ?, ?, ?)",
(
url,
expires,
stale,
blob.as_name(),
response.mimetype.as_deref().unwrap_or_default(),
response.encoding.as_deref().unwrap_or_default(),
),
)
.await?;
Ok(())
}
/// Retrieves the binary from HTTP cache.
///
/// Also returns if the response is stale and should be revalidated in the background.
async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response, bool)>> {
let now = time();
let Some((blob_name, mimetype, encoding, stale_timestamp)) = context
.sql
.query_row_optional(
"SELECT blobname, mimetype, encoding, stale
FROM http_cache WHERE url=? AND expires > ?",
(url, now),
|row| {
let blob_name: String = row.get(0)?;
let mimetype: Option<String> = Some(row.get(1)?).filter(|s: &String| !s.is_empty());
let encoding: Option<String> = Some(row.get(2)?).filter(|s: &String| !s.is_empty());
let stale_timestamp: i64 = row.get(3)?;
Ok((blob_name, mimetype, encoding, stale_timestamp))
},
)
.await?
else {
return Ok(None);
};
let is_stale = now > stale_timestamp;
let blob_object = BlobObject::from_name(context, blob_name)?;
let blob_abs_path = blob_object.to_abs_path();
let blob = match fs::read(blob_abs_path)
.await
.with_context(|| format!("Failed to read blob for {url:?} cache entry."))
{
Ok(blob) => blob,
Err(err) => {
// This should not happen, but user may go into the blobdir and remove files,
// antivirus may delete the file or there may be a bug in housekeeping.
warn!(context, "{err:?}.");
return Ok(None);
}
};
let (expires, _stale) = http_url_cache_timestamps(url, mimetype.as_deref());
let response = Response {
blob,
mimetype,
encoding,
};
// Update expiration timestamp
// to prevent deletion of the file still in use.
//
// If the response is stale, the caller should revalidate it in the background, so update
// `stale` timestamp to avoid revalidating too frequently (and have many parallel revalidation
// tasks) if revalidation fails or the HTTP request takes some time. The stale period >= 1 hour,
// so 1 more minute won't be a problem.
let stale_timestamp = if is_stale { now + 60 } else { stale_timestamp };
context
.sql
.execute(
"UPDATE http_cache SET expires=?, stale=? WHERE url=?",
(expires, stale_timestamp, url),
)
.await?;
Ok(Some((response, is_stale)))
}
/// Removes expired cache entries.
pub(crate) async fn http_cache_cleanup(context: &Context) -> Result<()> {
// Remove cache entries that are already expired
// or entries that will not expire in a year
// to make sure we don't have invalid timestamps that are way forward in the future.
context
.sql
.execute(
"DELETE FROM http_cache
WHERE ?1 > expires OR expires > ?1 + 31536000",
(time(),),
)
.await?;
Ok(())
}
/// Fetches URL and updates the cache.
///
/// URL is fetched regardless of whether there is an existing result in the cache.
async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
let mut url = original_url.to_string();
/// Retrieves the binary contents of URL using HTTP GET request.
pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
let mut url = url.to_string();
// Follow up to 10 http-redirects
for _i in 0..10 {
@@ -279,42 +139,16 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
});
let body = response.collect().await?.to_bytes();
let blob: Vec<u8> = body.to_vec();
let response = Response {
return Ok(Response {
blob,
mimetype,
encoding,
};
info!(context, "Inserting {original_url:?} into cache.");
http_cache_put(context, &url, &response).await?;
return Ok(response);
});
}
Err(anyhow!("Followed 10 redirections"))
}
/// Retrieves the binary contents of URL using HTTP GET request.
pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
if let Some((response, is_stale)) = http_cache_get(context, url).await? {
info!(context, "Returning {url:?} from cache.");
if is_stale {
let context = context.clone();
let url = url.to_string();
tokio::spawn(async move {
// Fetch URL in background to update the cache.
info!(context, "Fetching stale {url:?} in background.");
if let Err(err) = fetch_url(&context, &url).await {
warn!(context, "Failed to revalidate {url:?}: {err:#}.");
}
});
}
return Ok(response);
}
info!(context, "Not found {url:?} in cache, fetching.");
let response = fetch_url(context, url).await?;
Ok(response)
}
/// Sends an empty POST request to the URL.
///
/// Returns response text and whether request was successful or not.
@@ -407,125 +241,3 @@ pub(crate) async fn post_form<T: Serialize + ?Sized>(
let bytes = response.collect().await?.to_bytes();
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use crate::sql::housekeeping;
use crate::test_utils::TestContext;
use crate::tools::SystemTime;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_http_cache() -> Result<()> {
let t = &TestContext::new().await;
assert_eq!(http_cache_get(t, "https://webxdc.org/").await?, None);
let html_response = Response {
blob: b"<!DOCTYPE html> ...".to_vec(),
mimetype: Some("text/html".to_string()),
encoding: None,
};
let xdc_response = Response {
blob: b"PK...".to_vec(),
mimetype: Some("application/octet-stream".to_string()),
encoding: None,
};
let xdc_editor_url = "https://apps.testrun.org/webxdc-editor-v3.2.0.xdc";
let xdc_pixel_url = "https://apps.testrun.org/webxdc-pixel-v2.xdc";
http_cache_put(t, "https://webxdc.org/", &html_response).await?;
assert_eq!(http_cache_get(t, xdc_editor_url).await?, None);
assert_eq!(http_cache_get(t, xdc_pixel_url).await?, None);
assert_eq!(
http_cache_get(t, "https://webxdc.org/").await?,
Some((html_response.clone(), false))
);
http_cache_put(t, xdc_editor_url, &xdc_response).await?;
http_cache_put(t, xdc_pixel_url, &xdc_response).await?;
assert_eq!(
http_cache_get(t, xdc_editor_url).await?,
Some((xdc_response.clone(), false))
);
assert_eq!(
http_cache_get(t, xdc_pixel_url).await?,
Some((xdc_response.clone(), false))
);
assert_eq!(
http_cache_get(t, "https://webxdc.org/").await?,
Some((html_response.clone(), false))
);
// HTML is stale after 1 hour, but .xdc is not.
SystemTime::shift(Duration::from_secs(3600 + 100));
assert_eq!(
http_cache_get(t, "https://webxdc.org/").await?,
Some((html_response.clone(), true))
);
assert_eq!(
http_cache_get(t, xdc_editor_url).await?,
Some((xdc_response.clone(), false))
);
// Stale cache entry can be renewed
// even before housekeeping removes old one.
http_cache_put(t, "https://webxdc.org/", &html_response).await?;
assert_eq!(
http_cache_get(t, "https://webxdc.org/").await?,
Some((html_response.clone(), false))
);
// 35 days later pixel .xdc expires because we did not request it for 35 days and 1 hour.
// But editor is still there because we did not request it for just 35 days.
// We have not renewed the editor however, so it becomes stale.
SystemTime::shift(Duration::from_secs(3600 * 24 * 35 - 100));
// Run housekeeping to test that it does not delete the blob too early.
housekeeping(t).await?;
assert_eq!(
http_cache_get(t, xdc_editor_url).await?,
Some((xdc_response.clone(), true))
);
assert_eq!(http_cache_get(t, xdc_pixel_url).await?, None);
// If we get the blob the second time quickly, it shouldn't be stale because it's supposed
// that we've already run a revalidation task which will update the blob soon.
assert_eq!(
http_cache_get(t, xdc_editor_url).await?,
Some((xdc_response.clone(), false))
);
// But if the revalidation task hasn't succeeded after some time, the blob is stale again
// even if we continue to get it frequently.
for i in (0..100).rev() {
SystemTime::shift(Duration::from_secs(6));
if let Some((_, true)) = http_cache_get(t, xdc_editor_url).await? {
break;
}
assert!(i > 0);
}
// Test that if the file is accidentally removed from the blobdir,
// there is no error when trying to load the cache entry.
for entry in std::fs::read_dir(t.get_blobdir())? {
let entry = entry.unwrap();
let path = entry.path();
std::fs::remove_file(path).expect("Failed to remove blob");
}
assert_eq!(
http_cache_get(t, xdc_editor_url)
.await
.context("Failed to get no cache response")?,
None
);
Ok(())
}
}

View File

@@ -230,7 +230,7 @@ where
.get(9..12)
.context("HTTP status line does not contain a status code")?;
// Interpret status code according to
// Interpert status code according to
// <https://datatracker.ietf.org/doc/html/rfc7231#section-6>.
if status_code == b"407" {
Err(format_err!("Proxy Authentication Required"))
@@ -640,9 +640,6 @@ mod tests {
fn test_invalid_proxy_url() {
assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err());
assert!(ProxyConfig::from_url("abc").is_err());
// This caused panic before shadowsocks 1.22.0.
assert!(ProxyConfig::from_url("ss://foo:bar@127.0.0.1:9999").is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -55,8 +55,6 @@ pub enum Param {
/// For Messages: decrypted with validation errors or without mutual set, if neither
/// 'c' nor 'e' are preset, the messages is only transport encrypted.
///
/// Deprecated on 2024-12-25.
ErroneousE2ee = b'e',
/// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
@@ -183,8 +181,6 @@ pub enum Param {
GroupNameTimestamp = b'g',
/// For Chats: timestamp of member list update.
///
/// Deprecated 2025-01-07.
MemberListTimestamp = b'k',
/// For Webxdc Message Instances: Current document name
@@ -370,16 +366,20 @@ impl Params {
///
/// This parses the parameter value as a [ParamsFile] and than
/// tries to return a [BlobObject] for that file. If the file is
/// not yet a valid blob, one will be created by copying the file.
/// not yet a valid blob, one will be created by copying the file
/// only if `create` is set to `true`, otherwise an error is
/// returned.
///
/// Note that in the [ParamsFile::FsPath] case the blob can be
/// created without copying if the path already refers to a valid
/// blob. If so a [BlobObject] will be returned.
/// blob. If so a [BlobObject] will be returned regardless of the
/// `create` argument.
#[allow(clippy::needless_lifetimes)]
pub async fn get_blob<'a>(
&self,
key: Param,
context: &'a Context,
create: bool,
) -> Result<Option<BlobObject<'a>>> {
let val = match self.get(key) {
Some(val) => val,
@@ -387,7 +387,10 @@ impl Params {
};
let file = ParamsFile::from_param(context, val)?;
let blob = match file {
ParamsFile::FsPath(path) => BlobObject::new_from_path(context, &path).await?,
ParamsFile::FsPath(path) => match create {
true => BlobObject::new_from_path(context, &path).await?,
false => BlobObject::from_path(context, &path)?,
},
ParamsFile::Blob(blob) => blob,
};
Ok(Some(blob))
@@ -543,20 +546,23 @@ mod tests {
let path: PathBuf = p.get_path(Param::File, &t).unwrap().unwrap();
assert_eq!(path, fname);
// Blob does not exist yet, expect error.
assert!(p.get_blob(Param::File, &t, false).await.is_err());
fs::write(fname, b"boo").await.unwrap();
let blob = p.get_blob(Param::File, &t).await.unwrap().unwrap();
let blob = p.get_blob(Param::File, &t, true).await.unwrap().unwrap();
assert!(blob.as_file_name().starts_with("foo"));
// Blob in blobdir, expect blob.
let bar_path = t.get_blobdir().join("bar");
p.set(Param::File, bar_path.to_str().unwrap());
let blob = p.get_blob(Param::File, &t).await.unwrap().unwrap();
let blob = p.get_blob(Param::File, &t, false).await.unwrap().unwrap();
assert_eq!(blob, BlobObject::from_name(&t, "bar".to_string()).unwrap());
p.remove(Param::File);
assert!(p.get_file(Param::File, &t).unwrap().is_none());
assert!(p.get_path(Param::File, &t).unwrap().is_none());
assert!(p.get_blob(Param::File, &t).await.unwrap().is_none());
assert!(p.get_blob(Param::File, &t, false).await.unwrap().is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -7,15 +7,15 @@
//! when it's not required. Only when a webxdc subscribes to realtime data or when a reatlime message is sent,
//! the p2p machinery should be started.
//!
//! Adding peer channels to webxdc needs upfront negotiation of a topic and sharing of public keys so that
//! Adding peer channels to webxdc needs upfront negotation of a topic and sharing of public keys so that
//! nodes can connect to each other. The explicit approach is as follows:
//!
//! 1. We introduce a new [`IrohGossipTopic`](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
//! 1. We introduce a new [GossipTopic](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
//! securely generated on the initial webxdc sender's device. This message header is encrypted
//! and sent in the same message as the webxdc application.
//! 2. Whenever `joinRealtimeChannel().setListener()` or `joinRealtimeChannel().send()` is called by the webxdc application,
//! we start a routine to establish p2p connectivity and join the gossip swarm with Iroh.
//! 3. The first step of this routine is to introduce yourself with a regular message containing the [`IrohNodeAddr`](crate::headerdef::HeaderDef::IrohNodeAddr).
//! 3. The first step of this routine is to introduce yourself with a regular message containing the `IrohPublicKey`.
//! This message contains the users relay-server and public key.
//! Direct IP address is not included as this information can be persisted by email providers.
//! 4. After the announcement, the sending peer joins the gossip swarm with an empty list of peer IDs (as they don't know anyone yet).
@@ -78,14 +78,6 @@ impl Iroh {
self.endpoint.network_change().await
}
/// Closes the QUIC endpoint.
pub(crate) async fn close(self) -> Result<()> {
self.endpoint
.close(0u32.into(), b"")
.await
.context("Closing iroh endpoint failed")
}
/// Join a topic and create the subscriber loop for it.
///
/// If there is no gossip, create it.
@@ -293,36 +285,15 @@ impl Context {
}
/// Get or initialize the iroh peer channel.
pub async fn get_or_try_init_peer_channel(
&self,
) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
pub async fn get_or_try_init_peer_channel(&self) -> Result<&Iroh> {
if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
bail!("Attempt to get Iroh when realtime is disabled");
}
if let Ok(lock) = tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
self.iroh.read().await,
|opt_iroh| opt_iroh.as_ref(),
) {
return Ok(lock);
}
let lock = self.iroh.write().await;
match tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
lock,
|opt_iroh| opt_iroh.as_ref(),
) {
Ok(lock) => Ok(lock),
Err(mut lock) => {
let iroh = self.init_peer_channels().await?;
*lock = Some(iroh);
tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
lock,
|opt_iroh| opt_iroh.as_ref(),
)
.map_err(|_| anyhow!("Downgrade should succeed as we just stored `Some` value"))
}
}
let ctx = self.clone();
self.iroh
.get_or_try_init(|| async { ctx.init_peer_channels().await })
.await
}
}
@@ -417,6 +388,7 @@ async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeA
))
})
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await
@@ -654,6 +626,7 @@ mod tests {
break;
}
}
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
// Bob adds alice to gossip peers.
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
@@ -663,23 +636,13 @@ mod tests {
.map(|addr| addr.node_id)
.collect::<Vec<_>>();
let alice_iroh = alice.get_or_try_init_peer_channel().await.unwrap();
assert_eq!(
members,
vec![
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
.get_node_addr()
.await
.unwrap()
.node_id
]
vec![alice_iroh.get_node_addr().await.unwrap().node_id]
);
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
@@ -688,10 +651,7 @@ mod tests {
.unwrap();
// Alice sends ephemeral message
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
alice_iroh
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
.await
.unwrap();
@@ -710,9 +670,7 @@ mod tests {
}
}
// Bob sends ephemeral message
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.await
.unwrap();
@@ -741,20 +699,10 @@ mod tests {
assert_eq!(
members,
vec![
bob.get_or_try_init_peer_channel()
.await
.unwrap()
.get_node_addr()
.await
.unwrap()
.node_id
]
vec![bob_iroh.get_node_addr().await.unwrap().node_id]
);
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
.await
.unwrap();
@@ -772,12 +720,6 @@ mod tests {
}
}
}
// Calling stop_io() closes iroh endpoint as well,
// even though I/O was not started in this test.
assert!(alice.iroh.read().await.is_some());
alice.stop_io().await;
assert!(alice.iroh.read().await.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -819,6 +761,7 @@ mod tests {
.unwrap();
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
// Bob adds alice to gossip peers.
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
@@ -828,23 +771,13 @@ mod tests {
.map(|addr| addr.node_id)
.collect::<Vec<_>>();
let alice_iroh = alice.get_or_try_init_peer_channel().await.unwrap();
assert_eq!(
members,
vec![
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
.get_node_addr()
.await
.unwrap()
.node_id
]
vec![alice_iroh.get_node_addr().await.unwrap().node_id]
);
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
@@ -853,10 +786,7 @@ mod tests {
.unwrap();
// Alice sends ephemeral message
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
alice_iroh
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
.await
.unwrap();
@@ -881,9 +811,7 @@ mod tests {
.unwrap();
let bob_sequence_number = bob
.iroh
.read()
.await
.as_ref()
.get()
.unwrap()
.sequence_numbers
.lock()
@@ -892,9 +820,7 @@ mod tests {
leave_webxdc_realtime(bob, bob_webxdc.id).await.unwrap();
let bob_sequence_number_after = bob
.iroh
.read()
.await
.as_ref()
.get()
.unwrap()
.sequence_numbers
.lock()
@@ -903,9 +829,7 @@ mod tests {
// Check that sequence number is persisted when leaving the channel.
assert_eq!(bob_sequence_number, bob_sequence_number_after);
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
@@ -913,9 +837,7 @@ mod tests {
.await
.unwrap();
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.await
.unwrap();
@@ -934,20 +856,11 @@ mod tests {
}
}
// channel is only used to remember if an advertisement has been sent
// channel is only used to remeber if an advertisement has been sent
// bob for example does not change the channels because he never sends an
// advertisement
assert_eq!(
alice
.iroh
.read()
.await
.as_ref()
.unwrap()
.iroh_channels
.read()
.await
.len(),
alice.iroh.get().unwrap().iroh_channels.read().await.len(),
1
);
leave_webxdc_realtime(alice, alice_webxdc.id).await.unwrap();
@@ -957,9 +870,7 @@ mod tests {
.unwrap();
assert!(alice
.iroh
.read()
.await
.as_ref()
.get()
.unwrap()
.iroh_channels
.read()
@@ -1052,19 +963,19 @@ mod tests {
.await
.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
assert!(alice.ctx.iroh.get().is_none());
// creates iroh endpoint as side effect
send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
.await
.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
assert!(alice.ctx.iroh.get().is_none());
// creates iroh endpoint as side effect
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
assert!(alice.ctx.iroh.get().is_none());
// This internal function should return error
// if accidentally called with the setting disabled.

View File

@@ -711,25 +711,9 @@ impl Peerstate {
Origin::IncomingUnknownFrom,
)
.await?;
context
.sql
.transaction(|transaction| {
transaction.execute(
"UPDATE chats_contacts
SET remove_timestamp=MAX(add_timestamp+1, ?)
WHERE chat_id=? AND contact_id=?",
(timestamp, chat_id, contact_id),
)?;
transaction.execute(
"INSERT INTO chats_contacts
(chat_id, contact_id, add_timestamp)
VALUES (?1, ?2, ?3)
ON CONFLICT (chat_id, contact_id)
DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)",
(chat_id, new_contact_id, timestamp),
)?;
Ok(())
})
chat::remove_from_chat_contacts_table(context, *chat_id, contact_id)
.await?;
chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id])
.await?;
context.emit_event(EventType::ChatModified(*chat_id));

View File

@@ -1,24 +1,10 @@
//! # Push notifications module.
//!
//! This module is responsible for Apple Push Notification Service
//! and Firebase Cloud Messaging push notifications.
//!
//! It provides [`PushSubscriber`] type
//! which holds push notification token for the device,
//! shared by all accounts.
use std::sync::atomic::Ordering;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use base64::Engine as _;
use pgp::crypto::aead::AeadAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::ser::Serialize;
use rand::thread_rng;
use anyhow::Result;
use tokio::sync::RwLock;
use crate::context::Context;
use crate::key::DcKey;
/// Manages subscription to Apple Push Notification services.
///
@@ -38,85 +24,20 @@ pub struct PushSubscriber {
inner: Arc<RwLock<PushSubscriberState>>,
}
/// The key was generated with
/// `rsop generate-key --profile rfc9580`
/// and public key was extracted with `rsop extract-cert`.
const NOTIFIERS_PUBLIC_KEY: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----
xioGZ03cdhsAAAAg6PasQQylEuWAp9N5PXN93rqjZdqOqN3s9RJEU/K8FZzCsAYf
GwoAAABBBQJnTdx2AhsDAh4JCAsJCAcKDQwLBRUKCQgLAhYCIiEGiJJktnCmEtXa
qsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAAUfgg/sg0sR2mytzADFBpNAaY0Hyu
aru8ics3eUkeNn2ziL4ZsIMx+4mcM5POvD0PG9LtH8Rz/y9iItD0c2aoRBab7iri
/gDm6aQuj3xXgtAiXdaN9s+QPxR9gY/zG1t9iXgBzioGZ03cdhkAAAAgwJ0wQFsk
MGH4jklfK1fFhYoQZMjEFCRBIk+r1S+WaSDClQYYGwgAAAAsBQJnTdx2AhsMIiEG
iJJktnCmEtXaqsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAKCRCIkmS2cKYS1WdP
EFerccH2BoIPNbrxi6hwvxxy7G1mHg//ofD90fqmeY9xTfKMYl16bqQh4R1PiYd5
LMc5VqgXHgioqTYKbltlOtWC+HDt/PrymQsN4q/aEmsM
=5jvt
-----END PGP PUBLIC KEY BLOCK-----";
/// Pads the token with spaces.
///
/// This makes it impossible to tell
/// if the user is an Apple user with shorter tokens
/// or FCM user with longer tokens by the length of ciphertext.
fn pad_device_token(s: &str) -> String {
// 512 is larger than any token, tokens seen so far have not been larger than 200 bytes.
let expected_len: usize = 512;
let payload_len = s.len();
let padding_len = expected_len.saturating_sub(payload_len);
let padding = " ".repeat(padding_len);
let res = format!("{s}{padding}");
debug_assert_eq!(res.len(), expected_len);
res
}
/// Encrypts device token with OpenPGP.
///
/// The result is base64-encoded and not ASCII armored to avoid dealing with newlines.
pub(crate) fn encrypt_device_token(device_token: &str) -> Result<String> {
let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0;
let encryption_subkey = public_key
.public_subkeys
.first()
.context("No encryption subkey found")?;
let padded_device_token = pad_device_token(device_token);
let literal_message = pgp::composed::Message::new_literal("", &padded_device_token);
let mut rng = thread_rng();
let chunk_size = 8;
let encrypted_message = literal_message.encrypt_to_keys_seipdv2(
&mut rng,
SymmetricKeyAlgorithm::AES128,
AeadAlgorithm::Ocb,
chunk_size,
&[&encryption_subkey],
)?;
let encoded_message = encrypted_message.to_bytes()?;
Ok(format!(
"openpgp:{}",
base64::engine::general_purpose::STANDARD.encode(encoded_message)
))
}
impl PushSubscriber {
/// Creates new push notification subscriber.
pub(crate) fn new() -> Self {
Default::default()
}
/// Sets device token for Apple Push Notification service
/// or Firebase Cloud Messaging.
/// Sets device token for Apple Push Notification service.
pub(crate) async fn set_device_token(&self, token: &str) {
self.inner.write().await.device_token = Some(token.to_string());
}
/// Retrieves device token.
///
/// The token is encrypted with OpenPGP.
///
/// Token may be not available if application is not running on Apple platform,
/// does not have Google Play services,
/// failed to register for remote notifications or is in the process of registering.
///
/// IMAP loop should periodically check if device token is available
@@ -200,37 +121,3 @@ impl Context {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_device_token() {
let push_subscriber = PushSubscriber::new();
assert_eq!(push_subscriber.device_token().await, None);
push_subscriber.set_device_token("some-token").await;
let device_token = push_subscriber.device_token().await.unwrap();
assert_eq!(device_token, "some-token");
}
#[test]
fn test_pad_device_token() {
let apple_token = "0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894";
assert_eq!(pad_device_token(apple_token).trim(), apple_token);
}
#[test]
fn test_encrypt_device_token() {
let fcm_token = encrypt_device_token("fcm-chat.delta:c67DVcpVQN2rJHiSszKNDW:APA91bErcJV2b8qG0IT4aiuCqw6Al0_SbydSuz3V0CHBR1X7Fp8YzyvlpxNZIOGYVDFKejZGE1YiGSaqxmkr9ds0DuALmZNDwqIhuZWGKKrs3r7DTSkQ9MQ").unwrap();
let fcm_beta_token = encrypt_device_token("fcm-chat.delta.beta:chu-GhZCTLyzq1XseJp3na:APA91bFlsfDawdszWTyOLbxBy7KeRCrYM-SBFqutebF5ix0EZKMuCFUT_Y7R7Ex_eTQG_LbOu3Ky_z5UlTMJtI7ufpIp5wEvsFmVzQcOo3YhrUpbiSVGIlk").unwrap();
let apple_token = encrypt_device_token(
"0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894",
)
.unwrap();
assert_eq!(fcm_token.len(), fcm_beta_token.len());
assert_eq!(apple_token.len(), fcm_token.len());
}
}

View File

@@ -1,7 +1,6 @@
//! Internet Message Format reception pipeline.
use std::collections::HashSet;
use std::iter;
use std::str::FromStr;
use anyhow::{Context as _, Result};
@@ -15,7 +14,7 @@ use regex::Regex;
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::constants::{self, Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -26,16 +25,17 @@ use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
use crate::log::LogExt;
use crate::message::{
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
self, rfc724_mid_exists, rfc724_mid_exists_ex, Message, MessageState, MessengerMessage, MsgId,
Viewtype,
};
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
use crate::peerstate::Peerstate;
use crate::reaction::{set_msg_reaction, Reaction};
use crate::rusqlite::OptionalExtension;
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::simplify;
use crate::sql::{self, params_iter};
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{self, buf_compress, remove_subject_prefix};
@@ -73,7 +73,6 @@ pub struct ReceivedMsg {
///
/// This method returns errors on a failure to parse the mail or extract Message-ID. It's only used
/// for tests and REPL tool, not actual message reception pipeline.
#[cfg(any(test, feature = "internals"))]
pub async fn receive_imf(
context: &Context,
imf_raw: &[u8],
@@ -106,7 +105,6 @@ pub async fn receive_imf(
/// Emulates reception of a message from "INBOX".
///
/// Only used for tests and REPL tool, not actual message reception pipeline.
#[cfg(any(test, feature = "internals"))]
pub(crate) async fn receive_imf_from_inbox(
context: &Context,
rfc724_mid: &str,
@@ -345,18 +343,6 @@ pub(crate) async fn receive_imf_inner(
},
)
.await?;
let past_ids = add_or_lookup_contacts_by_address_list(
context,
&mime_parser.past_members,
if !mime_parser.incoming {
Origin::OutgoingTo
} else if incoming_origin.is_known() {
Origin::IncomingTo
} else {
Origin::IncomingUnknownTo
},
)
.await?;
update_verified_keys(context, &mut mime_parser, from_id).await?;
@@ -372,7 +358,7 @@ pub(crate) async fn receive_imf_inner(
let contact = Contact::get_by_id(context, from_id).await?;
mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
} else {
let to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
let to_id = to_ids.first().copied().unwrap_or_default();
// handshake may mark contacts as verified and must be processed before chats are created
res = observe_securejoin_on_other_device(context, &mime_parser, to_id)
.await
@@ -430,7 +416,6 @@ pub(crate) async fn receive_imf_inner(
&mut mime_parser,
imf_raw,
&to_ids,
&past_ids,
rfc724_mid_orig,
from_id,
seen,
@@ -453,10 +438,10 @@ pub(crate) async fn receive_imf_inner(
// and waste traffic.
let chat_id = received_msg.chat_id;
if !chat_id.is_special()
&& mime_parser.recipients.iter().all(|recipient| {
recipient.addr == mime_parser.from.addr
|| mime_parser.gossiped_keys.contains_key(&recipient.addr)
})
&& mime_parser
.recipients
.iter()
.all(|recipient| mime_parser.gossiped_keys.contains_key(&recipient.addr))
{
info!(
context,
@@ -620,7 +605,7 @@ pub(crate) async fn receive_imf_inner(
}
if let Some(replace_chat_id) = replace_chat_id {
context.emit_msgs_changed_without_msg_id(replace_chat_id);
context.emit_msgs_changed(replace_chat_id, MsgId::new(0));
} else if !chat_id.is_trash() {
let fresh = received_msg.state == MessageState::InFresh;
for msg_id in &received_msg.msg_ids {
@@ -634,11 +619,7 @@ pub(crate) async fn receive_imf_inner(
.await;
if let Some(is_bot) = mime_parser.is_bot {
// If the message is auto-generated and was generated by Delta Chat,
// mark the contact as a bot.
if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
from_id.mark_bot(context, is_bot).await?;
}
from_id.mark_bot(context, is_bot).await?;
}
Ok(Some(received_msg))
@@ -702,7 +683,6 @@ async fn add_parts(
mime_parser: &mut MimeMessage,
imf_raw: &[u8],
to_ids: &[ContactId],
past_ids: &[ContactId],
rfc724_mid: &str,
from_id: ContactId,
seen: bool,
@@ -792,11 +772,6 @@ async fn add_parts(
}
}
if chat_id.is_none() && is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is an MDN (TRASH).",);
}
if mime_parser.incoming {
to_id = ContactId::SELF;
@@ -808,6 +783,11 @@ async fn add_parts(
markseen_on_imap_table(context, rfc724_mid).await.ok();
}
if chat_id.is_none() && is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is an MDN (TRASH).",);
}
let create_blocked_default = if is_bot {
Blocked::Not
} else {
@@ -850,7 +830,6 @@ async fn add_parts(
create_blocked,
from_id,
to_ids,
past_ids,
&verified_encryption,
&grpid,
)
@@ -921,7 +900,7 @@ async fn add_parts(
group_chat_id,
from_id,
to_ids,
past_ids,
is_partial_download.is_some(),
&verified_encryption,
)
.await?;
@@ -961,11 +940,14 @@ async fn add_parts(
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
} else if allow_creation {
let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
.await
.context("Failed to get (new) chat for contact")?;
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
.context("Failed to get (new) chat for contact")
.log_err(context)
{
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
if let Some(chat_id) = chat_id {
@@ -989,6 +971,7 @@ async fn add_parts(
// the 1:1 chat accordingly.
let chat = match is_partial_download.is_none()
&& mime_parser.get_header(HeaderDef::SecureJoin).is_none()
&& !is_mdn
{
true => Some(Chat::load_from_db(context, chat_id).await?)
.filter(|chat| chat.typ == Chattype::Single),
@@ -1049,13 +1032,9 @@ async fn add_parts(
// the mail is on the IMAP server, probably it is also delivered.
// We cannot recreate other states (read, error).
state = MessageState::OutDelivered;
to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
to_id = to_ids.first().copied().unwrap_or_default();
// Older Delta Chat versions with core <=1.152.2 only accepted
// self-sent messages in Saved Messages with own address in the `To` field.
// New Delta Chat versions may use empty `To` field
// with only a single `hidden-recipients` group in this case.
let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
let self_sent = to_ids.len() == 1 && to_ids.contains(&ContactId::SELF);
if mime_parser.sync_items.is_some() && self_sent {
chat_id = Some(DC_CHAT_ID_TRASH);
@@ -1090,7 +1069,6 @@ async fn add_parts(
Blocked::Not,
from_id,
to_ids,
past_ids,
&verified_encryption,
&grpid,
)
@@ -1162,8 +1140,9 @@ async fn add_parts(
chat_id = Some(id);
chat_id_blocked = blocked;
}
} else {
let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
} else if let Ok(chat) =
ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await
{
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
@@ -1179,7 +1158,7 @@ async fn add_parts(
if chat_id_blocked != Blocked::Not {
if let Some(chat_id) = chat_id {
chat_id.unblock_ex(context, Nosync).await?;
// Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
chat_id_blocked = Blocked::Not;
}
}
}
@@ -1191,12 +1170,32 @@ async fn add_parts(
chat_id,
from_id,
to_ids,
past_ids,
is_partial_download.is_some(),
&verified_encryption,
)
.await?;
}
if chat_id.is_none() && self_sent {
// from_id==to_id==ContactId::SELF - this is a self-sent messages,
// maybe an Autocrypt Setup Message
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
.await
.context("Failed to get (new) chat for contact")
.log_err(context)
{
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked {
chat_id.unblock_ex(context, Nosync).await?;
// Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
}
}
}
if chat_id.is_none() {
// Check if the message belongs to a broadcast list.
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
@@ -1212,21 +1211,6 @@ async fn add_parts(
);
}
}
if chat_id.is_none() && self_sent {
// from_id==to_id==ContactId::SELF - this is a self-sent messages,
// maybe an Autocrypt Setup Message
let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
.await
.context("Failed to get (new) chat for contact")?;
chat_id = Some(chat.id);
// Not assigning `chat_id_blocked = chat.blocked` to avoid unused_assignments warning.
if Blocked::Not != chat.blocked {
chat.id.unblock_ex(context, Nosync).await?;
}
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
@@ -1246,7 +1230,7 @@ async fn add_parts(
}
let orig_chat_id = chat_id;
let mut chat_id = if is_reaction {
let mut chat_id = if is_mdn || is_reaction {
DC_CHAT_ID_TRASH
} else {
chat_id.unwrap_or_else(|| {
@@ -1370,7 +1354,7 @@ async fn add_parts(
// 1. They can't be an attack (they are outgoing, not incoming)
// 2. Probably the unencryptedness is just a temporary state, after all
// the user obviously still uses DC
// -> Showing info messages every time would be a lot of noise
// -> Showing info messages everytime would be a lot of noise
// 3. The info messages that are shown to the user ("Your chat partner
// likely reinstalled DC" or similar) would be wrong.
if chat.is_protected() && (mime_parser.incoming || chat.typ != Chattype::Single) {
@@ -1420,11 +1404,10 @@ async fn add_parts(
// we save the full mime-message and add a flag
// that the ui should show button to display the full message.
// We add "Show Full Message" button to the last message bubble (part) if this flag evaluates to
// `true` finally.
let mut save_mime_modified = false;
// a flag used to avoid adding "show full message" button to multiple parts of the message.
let mut save_mime_modified = mime_parser.is_mime_modified;
let mime_headers = if save_mime_headers || mime_parser.is_mime_modified {
let mime_headers = if save_mime_headers || save_mime_modified {
let headers = if !mime_parser.decoded_data.is_empty() {
mime_parser.decoded_data.clone()
} else {
@@ -1490,8 +1473,7 @@ async fn add_parts(
}
}
let mut parts = mime_parser.parts.iter().peekable();
while let Some(part) = parts.next() {
for part in &mime_parser.parts {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages;
@@ -1535,11 +1517,14 @@ async fn add_parts(
} else {
(&part.msg, part.typ)
};
let part_is_empty =
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
let save_mime_modified = save_mime_modified && parts.peek().is_none();
let mime_modified = save_mime_modified && !part_is_empty;
if mime_modified {
// Avoid setting mime_modified for more than one part.
save_mime_modified = false;
}
if part.typ == Viewtype::Text {
let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default();
@@ -1559,7 +1544,8 @@ async fn add_parts(
// If you change which information is skipped if the message is trashed,
// also change `MsgId::trash()` and `delete_expired_messages()`
let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
let trash =
chat_id.is_trash() || (is_location_kml && msg.is_empty() && typ == Viewtype::Text);
let row_id = context
.sql
@@ -1622,14 +1608,14 @@ RETURNING id
},
hidden,
part.bytes as isize,
if (save_mime_headers || save_mime_modified) && !trash {
if (save_mime_headers || mime_modified) && !trash {
mime_headers.clone()
} else {
Vec::new()
},
mime_in_reply_to,
mime_references,
save_mime_modified,
mime_modified,
part.error.as_deref().unwrap_or_default(),
ephemeral_timer,
ephemeral_timestamp,
@@ -1704,7 +1690,12 @@ RETURNING id
"Message has {icnt} parts and is assigned to chat #{chat_id}."
);
if !chat_id.is_trash() {
// new outgoing message from another device marks the chat as noticed.
if !mime_parser.incoming && !chat_id.is_special() {
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
}
if !is_mdn {
let mut chat = Chat::load_from_db(context, chat_id).await?;
// In contrast to most other update-timestamps,
@@ -1887,44 +1878,34 @@ async fn lookup_chat_or_create_adhoc_group(
if !contact_ids.contains(&from_id) {
contact_ids.push(from_id);
}
let trans_fn = |t: &mut rusqlite::Transaction| {
t.pragma_update(None, "query_only", "0")?;
t.execute(
"CREATE TEMP TABLE temp.contacts (
id INTEGER PRIMARY KEY
) STRICT",
(),
)?;
let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
for &id in &contact_ids {
stmt.execute((id,))?;
}
let val = t
.query_row(
if let Some((chat_id, blocked)) = context
.sql
.query_row_optional(
&format!(
"SELECT c.id, c.blocked
FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
WHERE m.hidden=0 AND c.grpid='' AND c.name=?
AND (SELECT COUNT(*) FROM chats_contacts
WHERE chat_id=c.id
AND add_timestamp >= remove_timestamp)=?
WHERE chat_id=c.id)=?
AND (SELECT COUNT(*) FROM chats_contacts
WHERE chat_id=c.id
AND contact_id NOT IN (SELECT id FROM temp.contacts)
AND add_timestamp >= remove_timestamp)=0
WHERE chat_id=c.id
AND contact_id NOT IN ({}))=0
ORDER BY m.timestamp DESC",
(&grpname, contact_ids.len()),
|row| {
let id: ChatId = row.get(0)?;
let blocked: Blocked = row.get(1)?;
Ok((id, blocked))
},
)
.optional()?;
t.execute("DROP TABLE temp.contacts", ())?;
Ok(val)
};
let query_only = true;
if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
sql::repeat_vars(contact_ids.len()),
),
rusqlite::params_from_iter(
params_iter(&[&grpname])
.chain(params_iter(&[contact_ids.len()]))
.chain(params_iter(&contact_ids)),
),
|row| {
let id: ChatId = row.get(0)?;
let blocked: Blocked = row.get(1)?;
Ok((id, blocked))
},
)
.await?
{
info!(
context,
"Assigning message to ad-hoc group {chat_id} with matching name and members."
@@ -1996,7 +1977,6 @@ async fn create_group(
create_blocked: Blocked,
from_id: ContactId,
to_ids: &[ContactId],
past_ids: &[ContactId],
verified_encryption: &VerifiedEncryption,
grpid: &str,
) -> Result<Option<(ChatId, Blocked)>> {
@@ -2070,37 +2050,14 @@ async fn create_group(
chat_id_blocked = create_blocked;
// Create initial member list.
if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
let mut new_to_ids = to_ids.to_vec();
if !new_to_ids.contains(&from_id) {
new_to_ids.insert(0, from_id);
chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
}
update_chats_contacts_timestamps(
context,
new_chat_id,
None,
&new_to_ids,
past_ids,
&chat_group_member_timestamps,
)
.await?;
} else {
let mut members = vec![ContactId::SELF];
if !from_id.is_special() {
members.push(from_id);
}
members.extend(to_ids);
chat::add_to_chat_contacts_table(
context,
mime_parser.timestamp_sent,
new_chat_id,
&members,
)
.await?;
let mut members = vec![ContactId::SELF];
if !from_id.is_special() {
members.push(from_id);
}
members.extend(to_ids);
members.sort_unstable();
members.dedup();
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
context.emit_event(EventType::ChatModified(new_chat_id));
chatlist_events::emit_chatlist_changed(context);
@@ -2125,86 +2082,13 @@ async fn create_group(
}
}
async fn update_chats_contacts_timestamps(
context: &Context,
chat_id: ChatId,
ignored_id: Option<ContactId>,
to_ids: &[ContactId],
past_ids: &[ContactId],
chat_group_member_timestamps: &[i64],
) -> Result<bool> {
let expected_timestamps_count = to_ids.len() + past_ids.len();
if chat_group_member_timestamps.len() != expected_timestamps_count {
warn!(
context,
"Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
chat_group_member_timestamps.len(),
expected_timestamps_count
);
return Ok(false);
}
let mut modified = false;
context
.sql
.transaction(|transaction| {
let mut add_statement = transaction.prepare(
"INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
VALUES (?1, ?2, ?3)
ON CONFLICT (chat_id, contact_id)
DO
UPDATE SET add_timestamp=?3
WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
)?;
for (contact_id, ts) in iter::zip(
to_ids.iter(),
chat_group_member_timestamps.iter().take(to_ids.len()),
) {
if Some(*contact_id) != ignored_id {
// It could be that member was already added,
// but updated addition timestamp
// is also a modification worth notifying about.
modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
}
}
let mut remove_statement = transaction.prepare(
"INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
VALUES (?1, ?2, ?3)
ON CONFLICT (chat_id, contact_id)
DO
UPDATE SET remove_timestamp=?3
WHERE ?3>remove_timestamp AND ?3>add_timestamp",
)?;
for (contact_id, ts) in iter::zip(
past_ids.iter(),
chat_group_member_timestamps.iter().skip(to_ids.len()),
) {
// It could be that member was already removed,
// but updated removal timestamp
// is also a modification worth notifying about.
modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
}
Ok(())
})
.await?;
Ok(modified)
}
/// Apply group member list, name, avatar and protection status changes from the MIME message.
///
/// Returns `Vec` of group changes messages and, optionally, a better message to replace the
/// original system message. If the better message is empty, the original system message
/// should be trashed.
/// original system message. If the better message is empty, the original system message should be
/// just omitted.
///
/// * `to_ids` - contents of the `To` and `Cc` headers.
/// * `past_ids` - contents of the `Chat-Group-Past-Members` header.
/// * `is_partial_download` - whether the message is not fully downloaded.
#[allow(clippy::too_many_arguments)]
async fn apply_group_changes(
context: &Context,
@@ -2212,7 +2096,7 @@ async fn apply_group_changes(
chat_id: ChatId,
from_id: ContactId,
to_ids: &[ContactId],
past_ids: &[ContactId],
is_partial_download: bool,
verified_encryption: &VerifiedEncryption,
) -> Result<(Vec<String>, Option<String>)> {
if chat_id.is_special() {
@@ -2241,6 +2125,49 @@ async fn apply_group_changes(
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
let is_from_in_chat =
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
// Reject group membership changes from non-members and old changes.
let member_list_ts = match !is_partial_download && is_from_in_chat {
true => Some(chat_id.get_member_list_timestamp(context).await?),
false => None,
};
// When we remove a member locally, we shift `MemberListTimestamp` by `TIMESTAMP_SENT_TOLERANCE`
// into the future, so add some more tolerance here to allow remote membership changes as well.
let timestamp_sent_tolerance = constants::TIMESTAMP_SENT_TOLERANCE * 2;
let allow_member_list_changes = member_list_ts
.filter(|t| {
*t <= mime_parser
.timestamp_sent
.saturating_add(timestamp_sent_tolerance)
})
.is_some();
let sync_member_list = member_list_ts
.filter(|t| *t <= mime_parser.timestamp_sent)
.is_some();
// Whether to rebuild the member list from scratch.
let recreate_member_list = {
// Always recreate membership list if SELF has been added. The older versions of DC
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
// delivered message (so it's a race), so we have this heuristic here.
self_added
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
// If we don't know the referenced message, we missed some messages.
// Maybe they added/removed members, so we need to recreate our member list.
Some(reply_to) => rfc724_mid_exists_ex(context, reply_to, "download_state=0")
.await?
.filter(|(_, _, downloaded)| *downloaded)
.is_none(),
None => false,
}
} && (
// Don't allow the timestamp tolerance here for more reliable leaving of groups.
sync_member_list || {
info!(
context,
"Ignoring a try to recreate member list of {chat_id} by {from_id}.",
);
false
}
);
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
@@ -2264,24 +2191,46 @@ async fn apply_group_changes(
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
if let Some(id) = removed_id {
better_msg = if id == from_id {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
if allow_member_list_changes && chat_contacts.contains(&id) {
better_msg = if id == from_id {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
}
} else {
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
added_id = Some(contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.");
better_msg.get_or_insert_with(Default::default);
if !allow_member_list_changes {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
);
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
if allow_member_list_changes {
let is_new_member;
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
if !recreate_member_list {
added_id = Some(contact_id);
}
is_new_member = !chat_contacts.contains(&contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.");
is_new_member = false;
}
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if is_new_member || self_added {
better_msg =
Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
}
} else {
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
}
better_msg.get_or_insert_with(Default::default);
} else if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.map(|s| s.trim())
@@ -2328,114 +2277,80 @@ async fn apply_group_changes(
}
}
// These are for adding info messages about implicit membership changes, so they are only
// filled when such messages are needed.
let mut added_ids = HashSet::<ContactId>::new();
let mut removed_ids = HashSet::<ContactId>::new();
if allow_member_list_changes {
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
new_members.insert(ContactId::SELF);
if !from_id.is_special() {
new_members.insert(from_id);
}
if is_from_in_chat {
if let Some(ref chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
send_event_chat_modified |= update_chats_contacts_timestamps(
if !recreate_member_list {
let mut diff = HashSet::<ContactId>::new();
if sync_member_list {
diff = new_members.difference(&chat_contacts).copied().collect();
} else if let Some(added_id) = added_id {
diff.insert(added_id);
}
new_members.clone_from(&chat_contacts);
// Don't delete any members locally, but instead add absent ones to provide group
// membership consistency for all members:
// - Classical MUA users usually don't intend to remove users from an email thread, so
// if they removed a recipient then it was probably by accident.
// - DC users could miss new member additions and then better to handle this in the same
// way as for classical MUA messages. Moreover, if we remove a member implicitly, they
// will never know that and continue to think they're still here.
// But it shouldn't be a big problem if somebody missed a member removal, because they
// will likely recreate the member list from the next received message. The problem
// occurs only if that "somebody" managed to reply earlier. Really, it's a problem for
// big groups with high message rate, but let it be for now.
new_members.extend(diff.clone());
if let Some(added_id) = added_id {
diff.remove(&added_id);
}
if !diff.is_empty() {
warn!(context, "Implicit addition of {diff:?} to chat {chat_id}.");
}
group_changes_msgs.reserve(diff.len());
for contact_id in diff {
let contact = Contact::get_by_id(context, contact_id).await?;
group_changes_msgs.push(
stock_str::msg_add_member_local(
context,
contact.get_addr(),
ContactId::UNDEFINED,
)
.await,
);
}
}
if let Some(removed_id) = removed_id {
new_members.remove(&removed_id);
}
if recreate_member_list {
info!(
context,
chat_id,
Some(from_id),
to_ids,
past_ids,
chat_group_member_timestamps,
)
.await?;
let new_chat_contacts = HashSet::<ContactId>::from_iter(
chat::get_chat_contacts(context, chat_id)
.await?
.iter()
.copied(),
"Recreating chat {chat_id} member list with {new_members:?}.",
);
added_ids = new_chat_contacts
.difference(&chat_contacts)
.copied()
.collect();
removed_ids = chat_contacts
.difference(&new_chat_contacts)
.copied()
.collect();
} else {
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
new_members.insert(ContactId::SELF);
if !from_id.is_special() {
new_members.insert(from_id);
}
}
if !self_added {
if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
// Allow non-Delta Chat MUAs to add members.
added_ids = new_members.difference(&chat_contacts).copied().collect();
}
if let Some(added_id) = added_id {
added_ids.insert(added_id);
}
new_members.clone_from(&chat_contacts);
// Don't delete any members locally, but instead add absent ones to provide group
// membership consistency for all members:
new_members.extend(added_ids.clone());
if new_members != chat_contacts {
chat::update_chat_contacts_table(context, chat_id, &new_members).await?;
chat_contacts = new_members;
send_event_chat_modified = true;
}
if sync_member_list {
let mut ts = mime_parser.timestamp_sent;
if recreate_member_list {
// Reject all older membership changes. See `allow_member_list_changes` to know how
// this works.
ts += timestamp_sent_tolerance;
}
if let Some(removed_id) = removed_id {
new_members.remove(&removed_id);
}
if new_members != chat_contacts {
chat::update_chat_contacts_table(
context,
mime_parser.timestamp_sent,
chat_id,
&new_members,
)
chat_id
.update_timestamp(context, Param::MemberListTimestamp, ts)
.await?;
chat_contacts = new_members;
send_event_chat_modified = true;
}
}
}
if let Some(added_id) = added_id {
if !added_ids.remove(&added_id) && !self_added {
// No-op "Member added" message.
//
// Trash it.
better_msg = Some(String::new());
}
}
if let Some(removed_id) = removed_id {
removed_ids.remove(&removed_id);
}
if !added_ids.is_empty() {
warn!(
context,
"Implicit addition of {added_ids:?} to chat {chat_id}."
);
}
if !removed_ids.is_empty() {
warn!(
context,
"Implicit removal of {removed_ids:?} from chat {chat_id}."
);
}
group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
for contact_id in added_ids {
let contact = Contact::get_by_id(context, contact_id).await?;
group_changes_msgs.push(
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
.await,
);
}
for contact_id in removed_ids {
let contact = Contact::get_by_id(context, contact_id).await?;
group_changes_msgs.push(
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
.await,
);
}
if let Some(avatar_action) = &mime_parser.group_avatar {
if !chat_contacts.contains(&ContactId::SELF) {
warn!(
@@ -2542,13 +2457,7 @@ async fn create_or_lookup_mailinglist(
)
})?;
chat::add_to_chat_contacts_table(
context,
mime_parser.timestamp_sent,
chat_id,
&[ContactId::SELF],
)
.await?;
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
Ok(Some((chat_id, blocked)))
} else {
info!(context, "Creating list forbidden by caller.");
@@ -2744,13 +2653,7 @@ async fn create_adhoc_group(
context,
"Created ad-hoc group id={new_chat_id}, name={grpname:?}."
);
chat::add_to_chat_contacts_table(
context,
mime_parser.timestamp_sent,
new_chat_id,
&member_ids,
)
.await?;
chat::add_to_chat_contacts_table(context, new_chat_id, &member_ids).await?;
context.emit_event(EventType::ChatModified(new_chat_id));
chatlist_events::emit_chatlist_changed(context);
@@ -2883,27 +2786,38 @@ async fn mark_recipients_as_verified(
to_ids: Vec<ContactId>,
mimeparser: &MimeMessage,
) -> Result<()> {
if to_ids.is_empty() {
return Ok(());
}
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
return Ok(());
}
let rows = context
.sql
.query_map(
&format!(
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
sql::repeat_vars(to_ids.len())
),
rusqlite::params_from_iter(&to_ids),
|row| {
let to_addr: String = row.get(0)?;
let is_verified: i32 = row.get(1).unwrap_or(0);
Ok((to_addr, is_verified != 0))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
let contact = Contact::get_by_id(context, from_id).await?;
for id in to_ids {
let Some((to_addr, is_verified)) = context
.sql
.query_row_optional(
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id=?",
(id,),
|row| {
let to_addr: String = row.get(0)?;
let is_verified: i32 = row.get(1).unwrap_or(0);
Ok((to_addr, is_verified != 0))
},
)
.await?
else {
continue;
};
for (to_addr, is_verified) in rows {
// mark gossiped keys (if any) as verified
if let Some(gossiped_key) = mimeparser.gossiped_keys.get(&to_addr.to_lowercase()) {
if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
@@ -3001,12 +2915,14 @@ pub(crate) async fn get_prefetch_parent_message(
}
/// Looks up contact IDs from the database given the list of recipients.
///
/// Returns vector of IDs guaranteed to be unique.
async fn add_or_lookup_contacts_by_address_list(
context: &Context,
address_list: &[SingleInfo],
origin: Origin,
) -> Result<Vec<ContactId>> {
let mut contact_ids = Vec::new();
let mut contact_ids = HashSet::new();
for info in address_list {
let addr = &info.addr;
if !may_be_valid_addr(addr) {
@@ -3017,13 +2933,13 @@ async fn add_or_lookup_contacts_by_address_list(
let (contact_id, _) =
Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
.await?;
contact_ids.push(contact_id);
contact_ids.insert(contact_id);
} else {
warn!(context, "Contact with address {:?} cannot exist.", addr);
}
}
Ok(contact_ids)
Ok(contact_ids.into_iter().collect::<Vec<ContactId>>())
}
#[cfg(test)]

View File

@@ -566,8 +566,6 @@ async fn test_escaped_recipients() {
.unwrap()
.0;
// We test with non-chat message here
// because chat messages are not expected to have `Cc` header.
receive_imf(
&t,
b"From: Foobar <foobar@example.com>\n\
@@ -575,6 +573,8 @@ async fn test_escaped_recipients() {
Cc: =?utf-8?q?=3Ch2=3E?= <carl@host.tld>\n\
Subject: foo\n\
Message-ID: <asdklfjjaweofi@example.com>\n\
Chat-Version: 1.0\n\
Chat-Disposition-Notification-To: <foobar@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
@@ -590,12 +590,11 @@ async fn test_escaped_recipients() {
let msg = Message::load_from_db(&t, chats.get_msg_id(0).unwrap().unwrap())
.await
.unwrap();
assert_eq!(msg.is_dc_message, MessengerMessage::No);
assert_eq!(msg.text, "foo hello");
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert_eq!(msg.text, "hello");
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
}
/// Tests that `Cc` header updates display name
/// if existing contact has low enough origin.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cc_to_contact() {
let t = TestContext::new_alice().await;
@@ -613,8 +612,6 @@ async fn test_cc_to_contact() {
.unwrap()
.0;
// We use non-chat message here
// because chat messages are not expected to have `Cc` header.
receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
@@ -623,6 +620,8 @@ async fn test_cc_to_contact() {
Cc: Carl <carl@host.tld>\n\
Subject: foo\n\
Message-ID: <asdklfjjaweofi@example.com>\n\
Chat-Version: 1.0\n\
Chat-Disposition-Notification-To: <foobar@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
@@ -869,10 +868,18 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
assert_eq!(msg.state, MessageState::OutFailed);
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await?;
let ChatItem::Message { msg_id } = *msgs.last().unwrap() else {
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
msg_id
} else {
panic!("Wrong item type");
};
assert_eq!(msg_id, msg.id);
let last_msg = Message::load_from_db(&t, *msg_id).await?;
assert_eq!(
last_msg.text,
stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com").await
);
assert_eq!(last_msg.from_id, ContactId::INFO);
Ok(())
}
@@ -2201,30 +2208,6 @@ Message content",
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
}
/// Tests that message with hidden recipients is assigned to Saved Messages chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hidden_recipients_self_chat() {
let t = TestContext::new_alice().await;
receive_imf(
&t,
b"Subject: s
Chat-Version: 1.0
Message-ID: <foobar@localhost>
To: hidden-recipients:;
From: <alice@example.org>
Message content",
false,
)
.await
.unwrap();
let msg = t.get_last_msg().await;
assert_eq!(msg.chat_id, t.get_self_chat().await.id);
assert_eq!(msg.to_id, ContactId::SELF);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_unencrypted_name_in_self_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -3334,7 +3317,6 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
chat::add_to_chat_contacts_table(
&bob,
time(),
group_id,
&[
bob.add_or_lookup_contact(&alice1).await.id,
@@ -3544,27 +3526,26 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
tcm.section("Bob creates a group");
// =============== Bob creates a group ===============
let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
chat::add_to_chat_contacts_table(
&bob,
time(),
group_id,
&[bob.add_or_lookup_contact(&alice).await.id],
)
.await?;
tcm.section("Bob sends the first message to the group");
// =============== Bob sends the first message to the group ===============
let sent = bob.send_text(group_id, "Hello all!").await;
alice.recv_msg(&sent).await;
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
assert_eq!(chats.len(), 1);
tcm.section("Bob blocks Alice");
// =============== Bob blocks Alice ================
Contact::block(&bob, bob.add_or_lookup_contact(&alice).await.id).await?;
tcm.section("Alice replies private to Bob");
// =============== Alice replies private to Bob ==============
let received = alice.get_last_msg().await;
assert_eq!(received.text, "Hello all!");
@@ -3578,7 +3559,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let sent2 = alice.send_msg(alice_bob_chat.id, &mut msg_out).await;
bob.recv_msg(&sent2).await;
// check that no contact request was created
// ========= check that no contact request was created ============
let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -3589,7 +3570,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let received = bob.get_last_msg().await;
assert_eq!(received.text, "Hello all!");
tcm.section("Bob unblocks Alice");
// =============== Bob unblocks Alice ================
// test if the blocked chat is restored correctly
Contact::unblock(&bob, bob.add_or_lookup_contact(&alice).await.id).await?;
let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap();
@@ -3854,61 +3835,6 @@ async fn test_messed_up_message_id() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_big_forwarded_with_big_attachment() -> Result<()> {
let t = &TestContext::new_bob().await;
let raw = include_bytes!("../../test-data/message/big_forwarded_with_big_attachment.eml");
let rcvd = receive_imf(t, raw, false).await?.unwrap();
assert_eq!(rcvd.msg_ids.len(), 3);
let msg = Message::load_from_db(t, rcvd.msg_ids[0]).await?;
assert_eq!(msg.get_viewtype(), Viewtype::Text);
assert_eq!(msg.get_text(), "Hello!");
assert!(!msg.has_html());
let msg = Message::load_from_db(t, rcvd.msg_ids[1]).await?;
assert_eq!(msg.get_viewtype(), Viewtype::Text);
assert!(msg
.get_text()
.starts_with("this text with 42 chars is just repeated."));
assert!(msg.get_text().ends_with("[...]"));
assert!(!msg.has_html());
let msg = Message::load_from_db(t, rcvd.msg_ids[2]).await?;
assert_eq!(msg.get_viewtype(), Viewtype::File);
assert!(msg.has_html());
let html = msg.id.get_html(t).await?.unwrap();
let tail = html
.split_once("Hello!")
.unwrap()
.1
.split_once("From: AAA")
.unwrap()
.1
.split_once("aaa@example.org")
.unwrap()
.1
.split_once("To: Alice")
.unwrap()
.1
.split_once("alice@example.org")
.unwrap()
.1
.split_once("Subject: Some subject")
.unwrap()
.1
.split_once("Date: Fri, 2 Jun 2023 12:29:17 +0000")
.unwrap()
.1;
assert_eq!(
tail.matches("this text with 42 chars is just repeated.")
.count(),
128
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mua_user_adds_member() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -4107,7 +4033,7 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
alice.pop_sent_msg().await;
// re-add bob
// readd bob
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
let add2 = alice.pop_sent_msg().await;
bob.recv_msg(&add2).await;
@@ -4154,15 +4080,11 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> {
SystemTime::shift(Duration::from_secs(3600));
// Bob replies again, even after some time this does not add Alice back.
//
// Bob cannot learn from Alice that Alice has left the group
// because Alice is not going to send more messages to the group.
// Bob replies again adding Alice back.
send_text_msg(bob, bob_chat_id, "i'm bob".to_string()).await?;
let msg = &bob.pop_sent_msg().await;
alice.recv_msg(msg).await;
assert!(!is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
assert!(is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
Ok(())
}
@@ -4223,7 +4145,7 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delayed_removal_is_ignored() -> Result<()> {
async fn test_recreate_contact_list_on_missing_message() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
@@ -4231,7 +4153,6 @@ async fn test_delayed_removal_is_ignored() -> Result<()> {
// create chat with three members
add_to_chat_contacts_table(
&alice,
time(),
chat_id,
&[
Contact::create(&alice, "bob", "bob@example.net").await?,
@@ -4244,45 +4165,37 @@ async fn test_delayed_removal_is_ignored() -> Result<()> {
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(&bob).await?;
// Bob removes Fiona.
// bob removes a member
let bob_contact_fiona = Contact::create(&bob, "fiona", "fiona@example.net").await?;
remove_contact_from_chat(&bob, bob_chat_id, bob_contact_fiona).await?;
let remove_msg = bob.pop_sent_msg().await;
// Bob adds new members "blue" and "orange", but first addition message is lost.
// bob adds a new member
let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?;
bob.pop_sent_msg().await;
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
let add_msg = bob.pop_sent_msg().await;
// Alice only receives the second member addition,
// but this results in addition of both members
// and removal of Fiona.
// alice only receives the addition of the member
alice.recv_msg(&add_msg).await;
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
// Alice re-adds Fiona.
// since we missed a message, a new contact list should be build
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 3);
// readd fiona
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5);
// Delayed removal of Fiona by Bob shouldn't remove her.
alice.recv_msg(&remove_msg).await;
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5);
alice
.golden_test_chat(chat_id, "receive_imf_delayed_removal_is_ignored")
.await;
// delayed removal of fiona shouldn't remove her
alice.recv_msg_trash(&remove_msg).await;
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_readd_with_normal_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
@@ -4297,7 +4210,6 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(&bob).await?;
// Bob leaves, but Alice didn't receive Bob's leave message.
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
bob.pop_sent_msg().await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
@@ -4311,11 +4223,12 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
.await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
// Bob received a message from Alice, but this should not re-add him to the group.
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
// Bob got an update that fiora is added nevertheless.
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
// Alice didn't receive Bob's leave message although a lot of time has
// passed, so Bob must readd themselves otherwise other members would think
// Bob is still here while they aren't. Bob should retry to leave if they
// think that Alice didn't re-add them on purpose (which is possible if Alice uses a classical
// MUA).
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
Ok(())
}
@@ -4542,15 +4455,6 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
send_text_msg(&alice, alice_chat_id, "4th message".to_string()).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
// Even if some time passed, Bob must not be re-added back.
SystemTime::shift(Duration::from_secs(3600));
send_text_msg(&alice, alice_chat_id, "5th message".to_string()).await?;
alice.pop_sent_msg().await;
send_text_msg(&alice, alice_chat_id, "6th message".to_string()).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
Ok(())
}
@@ -4784,6 +4688,13 @@ async fn test_partial_group_consistency() -> Result<()> {
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 2);
// Get initial timestamp.
let timestamp = bob_chat_id
.get_param(&bob)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap();
// Bob receives partial message.
let msg_id = receive_imf_from_inbox(
&bob,
@@ -4804,9 +4715,15 @@ Chat-Group-Member-Added: charlie@example.com",
.context("no received message")?;
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
let timestamp2 = bob_chat_id
.get_param(&bob)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap();
// Partial download does not change the member list.
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(timestamp, timestamp2);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
// Alice sends normal message to bob, adding fiona.
@@ -4819,6 +4736,15 @@ Chat-Group-Member-Added: charlie@example.com",
bob.recv_msg(&alice.pop_sent_msg().await).await;
let timestamp3 = bob_chat_id
.get_param(&bob)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap();
// Receiving a message after a partial download recreates the member list because we treat
// such messages as if we have not seen them.
assert_ne!(timestamp, timestamp3);
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 3);
@@ -4842,9 +4768,15 @@ Chat-Group-Member-Added: charlie@example.com",
.context("no received message")?;
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
let timestamp4 = bob_chat_id
.get_param(&bob)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap();
// After full download, the old message should not change group state.
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(timestamp3, timestamp4);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
Ok(())
@@ -4867,13 +4799,19 @@ async fn test_leave_protected_group_missing_member_key() -> Result<()> {
("b@b", "bob@example.net"),
)
.await?;
// We fail to send the message.
assert!(remove_contact_from_chat(alice, group_id, ContactId::SELF)
.await
.is_err());
// The contact is already removed anyway.
assert!(is_contact_in_chat(alice, group_id, ContactId::SELF).await?);
alice
.sql
.execute(
"UPDATE acpeerstates SET addr=? WHERE addr=?",
("bob@example.net", "b@b"),
)
.await?;
remove_contact_from_chat(alice, group_id, ContactId::SELF).await?;
alice.pop_sent_msg().await;
assert!(!is_contact_in_chat(alice, group_id, ContactId::SELF).await?);
Ok(())
}
@@ -4895,22 +4833,12 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
.await?;
let fiona = &tcm.fiona().await;
let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
mark_as_verified(alice, fiona).await;
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
assert!(add_contact_to_chat(alice, group_id, alice_fiona_id)
.await
.is_err());
// Sending the message failed,
// but member is added to the chat locally already.
assert!(is_contact_in_chat(alice, group_id, alice_fiona_id).await?);
let msg = alice.get_last_msg_in(group_id).await;
assert!(msg.is_info());
assert_eq!(
msg.get_text(),
stock_str::msg_add_member_local(alice, &fiona_addr, ContactId::SELF).await
);
assert!(!is_contact_in_chat(alice, group_id, alice_fiona_id).await?);
// Now the chat has a message "You added member fiona@example.net. [INFO] !!" (with error) that
// may be confusing, but if the error is displayed in UIs, it's more or less ok. This is not a
// normal scenario anyway.

View File

@@ -14,31 +14,12 @@ use crate::{context::Context, log::LogExt};
use super::InnerSchedulerState;
/// Rough connectivity status for display in the status bar in the UI.
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
pub enum Connectivity {
/// Not connected.
///
/// This may be because we just started,
/// because we lost connection and
/// were not able to connect and log in yet
/// or because I/O is not started.
NotConnected = 1000,
/// Attempting to connect and log in.
Connecting = 2000,
/// Fetching or sending messages.
/// Fetching or sending messages
Working = 3000,
/// We are connected but not doing anything.
///
/// This is the most common state,
/// so mobile UIs display the profile name
/// instead of connectivity status in this state.
/// Desktop UI displays "Connected" in the tooltip,
/// which signals that no more messages
/// are coming in.
Connected = 4000,
}
@@ -51,17 +32,13 @@ enum DetailedConnectivity {
Error(String),
#[default]
Uninitialized,
/// Attempting to connect,
/// until we successfully log in.
Connecting,
/// Connection is just established,
/// there may be work to do.
Preparing,
/// Connection is just established, but there may be work to do.
Connected,
/// There is actual work to do, e.g. there are messages in SMTP queue
/// or we detected a message on IMAP server that should be downloaded.
/// or we detected a message that should be downloaded.
Working,
InterruptingIdle,
@@ -80,14 +57,8 @@ impl DetailedConnectivity {
DetailedConnectivity::Uninitialized => Some(Connectivity::NotConnected),
DetailedConnectivity::Connecting => Some(Connectivity::Connecting),
DetailedConnectivity::Working => Some(Connectivity::Working),
DetailedConnectivity::InterruptingIdle => Some(Connectivity::Working),
// At this point IMAP has just connected,
// but does not know yet if there are messages to download.
// We still convert this to Working state
// so user can see "Updating..." and not "Connected"
// which is reserved for idle state.
DetailedConnectivity::Preparing => Some(Connectivity::Working),
DetailedConnectivity::InterruptingIdle => Some(Connectivity::Connected),
DetailedConnectivity::Connected => Some(Connectivity::Connected),
// Just don't return a connectivity, probably the folder is configured not to be
// watched or there is e.g. no "Sent" folder, so we are not interested in it
@@ -103,9 +74,9 @@ impl DetailedConnectivity {
| DetailedConnectivity::Uninitialized
| DetailedConnectivity::NotConfigured => "<span class=\"red dot\"></span>".to_string(),
DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(),
DetailedConnectivity::Preparing
| DetailedConnectivity::Working
DetailedConnectivity::Working
| DetailedConnectivity::InterruptingIdle
| DetailedConnectivity::Connected
| DetailedConnectivity::Idle => "<span class=\"green dot\"></span>".to_string(),
}
}
@@ -115,12 +86,10 @@ impl DetailedConnectivity {
DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
DetailedConnectivity::Uninitialized => "Not started".to_string(),
DetailedConnectivity::Connecting => stock_str::connecting(context).await,
DetailedConnectivity::Preparing | DetailedConnectivity::Working => {
stock_str::updating(context).await
}
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
stock_str::connected(context).await
}
DetailedConnectivity::Working => stock_str::updating(context).await,
DetailedConnectivity::InterruptingIdle
| DetailedConnectivity::Connected
| DetailedConnectivity::Idle => stock_str::connected(context).await,
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
}
}
@@ -138,7 +107,7 @@ impl DetailedConnectivity {
// since sending the last message, connectivity could have changed, which we don't notice
// until another message is sent
DetailedConnectivity::InterruptingIdle
| DetailedConnectivity::Preparing
| DetailedConnectivity::Connected
| DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context).await,
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
}
@@ -151,7 +120,7 @@ impl DetailedConnectivity {
DetailedConnectivity::Connecting => false,
DetailedConnectivity::Working => false,
DetailedConnectivity::InterruptingIdle => false,
DetailedConnectivity::Preparing => false, // Just connected, there may still be work to do.
DetailedConnectivity::Connected => false, // Just connected, there may still be work to do.
DetailedConnectivity::NotConfigured => true,
DetailedConnectivity::Idle => true,
}
@@ -179,8 +148,8 @@ impl ConnectivityStore {
pub(crate) async fn set_working(&self, context: &Context) {
self.set(context, DetailedConnectivity::Working).await;
}
pub(crate) async fn set_preparing(&self, context: &Context) {
self.set(context, DetailedConnectivity::Preparing).await;
pub(crate) async fn set_connected(&self, context: &Context) {
self.set(context, DetailedConnectivity::Connected).await;
}
pub(crate) async fn set_not_configured(&self, context: &Context) {
self.set(context, DetailedConnectivity::NotConfigured).await;
@@ -200,8 +169,8 @@ impl ConnectivityStore {
}
}
/// Set all folder states to InterruptingIdle in case they were `Idle` before.
/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
/// Set all folder states to InterruptingIdle in case they were `Connected` before.
/// Called during `dc_maybe_network()` to make sure that `dc_all_work_done()`
/// returns false immediately after `dc_maybe_network()`.
pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
let mut connectivity_lock = inbox.0.lock().await;
@@ -210,7 +179,8 @@ pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<Conne
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
// return Connected until DC is completely done with fetching folders; this also
// includes scan_folders() which happens on the inbox thread.
if *connectivity_lock == DetailedConnectivity::Idle
if *connectivity_lock == DetailedConnectivity::Connected
|| *connectivity_lock == DetailedConnectivity::Idle
|| *connectivity_lock == DetailedConnectivity::NotConfigured
{
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
@@ -219,7 +189,9 @@ pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<Conne
for state in oboxes {
let mut connectivity_lock = state.0.lock().await;
if *connectivity_lock == DetailedConnectivity::Idle {
if *connectivity_lock == DetailedConnectivity::Connected
|| *connectivity_lock == DetailedConnectivity::Idle
{
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
}
}
@@ -535,7 +507,7 @@ impl Context {
}
/// Returns true if all background work is done.
async fn all_work_done(&self) -> bool {
pub async fn all_work_done(&self) -> bool {
let lock = self.scheduler.inner.read().await;
let stores: Vec<_> = match *lock {
InnerSchedulerState::Started(ref sched) => sched
@@ -555,23 +527,4 @@ impl Context {
}
true
}
/// Waits until background work is finished.
pub async fn wait_for_all_work_done(&self) {
// Ideally we could wait for connectivity change events,
// but sleep loop is good enough.
// First 100 ms sleep in chunks of 10 ms.
for _ in 0..10 {
if self.all_work_done().await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
// If we are not finished in 100 ms, keep waking up every 100 ms.
while !self.all_work_done().await {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
}

View File

@@ -454,9 +454,6 @@ pub(crate) async fn handle_securejoin_handshake(
.await?;
inviter_progress(context, contact_id, 800);
inviter_progress(context, contact_id, 1000);
// IMAP-delete the message to avoid handling it by another device and adding the
// member twice. Another device will know the member's key from Autocrypt-Gossip.
Ok(HandshakeMessage::Done)
} else {
// Setup verified contact.
secure_connection_established(
@@ -471,8 +468,8 @@ pub(crate) async fn handle_securejoin_handshake(
.context("failed sending vc-contact-confirm message")?;
inviter_progress(context, contact_id, 1000);
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
}
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
}
/*=======================================================
==== Bob - the joiner's side ====
@@ -754,7 +751,7 @@ mod tests {
use crate::imex::{imex, ImexMode};
use crate::receive_imf::receive_imf;
use crate::stock_str::{self, chat_protection_enabled};
use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote};
use crate::test_utils::get_chat_msg;
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::SystemTime;
use std::collections::HashSet;
@@ -801,8 +798,6 @@ mod tests {
}
async fn test_setup_contact_ex(case: SetupContactCase) {
let _n = TimeShiftFalsePositiveNote;
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
@@ -1356,8 +1351,6 @@ mod tests {
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
// be strange to have it in "member-added" messages of verified groups only.
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
{
// Now Alice's chat with Bob should still be hidden, the verified message should

View File

@@ -59,13 +59,8 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// only become usable once the protocol is finished.
let group_chat_id = state.joining_chat_id(context).await?;
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(
context,
time(),
group_chat_id,
&[invite.contact_id()],
)
.await?;
chat::add_to_chat_contacts_table(context, group_chat_id, &[invite.contact_id()])
.await?;
}
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
chat::add_info_msg(context, group_chat_id, &msg, time()).await?;

View File

@@ -21,6 +21,7 @@ use crate::mimefactory::MimeFactory;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionBufStream;
use crate::scheduler::connectivity::ConnectivityStore;
use crate::sql;
use crate::stock_str::unencrypted_email;
use crate::tools::{self, time_elapsed};
@@ -584,16 +585,18 @@ async fn send_mdn_rfc724_mid(
info!(context, "Successfully sent MDN for {rfc724_mid}.");
context
.sql
.transaction(|transaction| {
let mut stmt =
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
stmt.execute((rfc724_mid,))?;
for additional_rfc724_mid in additional_rfc724_mids {
stmt.execute((additional_rfc724_mid,))?;
}
Ok(())
})
.execute("DELETE FROM smtp_mdns WHERE rfc724_mid = ?", (rfc724_mid,))
.await?;
if !additional_rfc724_mids.is_empty() {
let q = format!(
"DELETE FROM smtp_mdns WHERE rfc724_mid IN({})",
sql::repeat_vars(additional_rfc724_mids.len())
);
context
.sql
.execute(&q, rusqlite::params_from_iter(additional_rfc724_mids))
.await?;
}
Ok(true)
}
SendResult::Retry => {

View File

@@ -19,7 +19,6 @@ use crate::location::delete_orphaned_poi_locations;
use crate::log::LogExt;
use crate::message::{Message, MsgId};
use crate::net::dns::prune_dns_cache;
use crate::net::http::http_cache_cleanup;
use crate::net::prune_connection_history;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
@@ -44,6 +43,12 @@ macro_rules! params_slice {
};
}
pub(crate) fn params_iter(
iter: &[impl crate::sql::ToSql],
) -> impl Iterator<Item = &dyn crate::sql::ToSql> {
iter.iter().map(|item| item as &dyn crate::sql::ToSql)
}
mod migrations;
mod pool;
@@ -435,7 +440,7 @@ impl Sql {
.await
}
/// Execute the function inside a transaction assuming that it does writes.
/// Execute the function inside a transaction assuming that it does write queries.
///
/// If the function returns an error, the transaction will be rolled back. If it does not return an
/// error, the transaction will be committed.
@@ -444,28 +449,7 @@ impl Sql {
H: Send + 'static,
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
{
let query_only = false;
self.transaction_ex(query_only, callback).await
}
/// Execute the function inside a transaction.
///
/// * `query_only` - Whether the function only executes read statements (queries) and can be run
/// in parallel with other transactions. NB: Creating and modifying temporary tables are also
/// allowed with `query_only`, temporary tables aren't visible in other connections, but you
/// need to pass `PRAGMA query_only=0;` to SQLite before that:
/// `pragma_update(None, "query_only", "0")`.
/// Also temporary tables need to be dropped because the connection is returned to the pool
/// then.
///
/// If the function returns an error, the transaction will be rolled back. If it does not return
/// an error, the transaction will be committed.
pub async fn transaction_ex<G, H>(&self, query_only: bool, callback: G) -> Result<H>
where
H: Send + 'static,
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
{
self.call(query_only, move |conn| {
self.call_write(move |conn| {
let mut transaction = conn.transaction()?;
let ret = callback(&mut transaction);
@@ -736,12 +720,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
warn!(context, "Can't set config: {e:#}.");
}
http_cache_cleanup(context)
.await
.context("Failed to cleanup HTTP cache")
.log_err(context)
.ok();
if let Err(err) = remove_unused_files(context).await {
warn!(
context,
@@ -868,22 +846,6 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
.await
.context("housekeeping: failed to SELECT value FROM config")?;
context
.sql
.query_map(
"SELECT blobname FROM http_cache",
(),
|row| row.get::<_, String>(0),
|rows| {
for row in rows {
maybe_add_file(&mut files_in_use, &row?);
}
Ok(())
},
)
.await
.context("Failed to SELECT blobname FROM http_cache")?;
info!(context, "{} files in use.", files_in_use.len());
/* go through directories and delete unused files */
let blobdir = context.get_blobdir();
@@ -902,6 +864,7 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
if p == blobdir
&& (is_file_in_use(&files_in_use, None, &name_s)
|| is_file_in_use(&files_in_use, Some(".increation"), &name_s)
|| is_file_in_use(&files_in_use, Some(".waveform"), &name_s)
|| is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s))
{
@@ -924,11 +887,13 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
}
unreferenced_count += 1;
let recently_created =
stats.created().is_ok_and(|t| t > keep_files_newer_than);
let recently_modified =
stats.modified().is_ok_and(|t| t > keep_files_newer_than);
let recently_accessed =
stats.accessed().is_ok_and(|t| t > keep_files_newer_than);
stats.created().map_or(false, |t| t > keep_files_newer_than);
let recently_modified = stats
.modified()
.map_or(false, |t| t > keep_files_newer_than);
let recently_accessed = stats
.accessed()
.map_or(false, |t| t > keep_files_newer_than);
if p == blobdir
&& (recently_created || recently_modified || recently_accessed)
@@ -1039,6 +1004,16 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
Ok(())
}
/// Helper function to return comma-separated sequence of `?` chars.
///
/// Use this together with [`rusqlite::ParamsFromIter`] to use dynamically generated
/// parameter lists.
pub fn repeat_vars(count: usize) -> String {
let mut s = "?,".repeat(count);
s.pop(); // Remove trailing comma
s
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1070,107 +1070,6 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
.await?;
}
inc_and_check(&mut migration_version, 124)?;
if dbversion < migration_version {
// Mark Saved Messages chat as protected if it already exists.
sql.execute_migration(
"UPDATE chats
SET protected=1 -- ProtectionStatus::Protected
WHERE type==100 -- Chattype::Single
AND EXISTS (
SELECT 1 FROM chats_contacts cc
WHERE cc.chat_id==chats.id
AND cc.contact_id=1
)
",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 125)?;
if dbversion < migration_version {
sql.execute_migration(
"CREATE TABLE http_cache (
url TEXT PRIMARY KEY,
expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
blobname TEXT NOT NULL,
mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
) STRICT",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 126)?;
if dbversion < migration_version {
// Recreate http_cache table with new `stale` column.
sql.execute_migration(
"DROP TABLE http_cache;
CREATE TABLE http_cache (
url TEXT PRIMARY KEY,
expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
stale INTEGER NOT NULL, -- When the cache entry is considered stale, timestamp in seconds.
blobname TEXT NOT NULL,
mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
) STRICT",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 127)?;
if dbversion < migration_version {
// This is buggy: `delete_server_after` > 1 isn't handled. Migration #129 fixes this.
sql.execute_migration(
"INSERT OR IGNORE INTO config (keyname, value)
SELECT 'bcc_self', '1'
FROM config WHERE keyname='delete_server_after' AND value='0'
",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 128)?;
if dbversion < migration_version {
// Add the timestamps of addition and removal.
//
// If `add_timestamp >= remove_timestamp`,
// then the member is currently a member of the chat.
// Otherwise the member is a past member.
sql.execute_migration(
"ALTER TABLE chats_contacts
ADD COLUMN add_timestamp NOT NULL DEFAULT 0;
ALTER TABLE chats_contacts
ADD COLUMN remove_timestamp NOT NULL DEFAULT 0;
",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 129)?;
if dbversion < migration_version {
// Existing chatmail configurations having `delete_server_after` != "delete at once" should
// get `bcc_self` enabled, they may be multidevice configurations:
// - Before migration #127, `delete_server_after` was set to 0 upon a backup export, but
// then `bcc_self` is enabled instead (whose default is changed to 0 for chatmail).
// - The user might set `delete_server_after` to a value other than 0 or 1 when that was
// possible in UIs.
// We don't check `is_chatmail` for simplicity.
sql.execute_migration(
"INSERT OR IGNORE INTO config (keyname, value)
SELECT 'bcc_self', '1'
FROM config WHERE keyname='delete_server_after' AND value!='1'
",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
@@ -1231,35 +1130,6 @@ impl Sql {
.await
.with_context(|| format!("execute_migration failed for version {version}"))?;
self.config_cache.write().await.clear();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_clear_config_cache() -> anyhow::Result<()> {
// Some migrations change the `config` table in SQL.
// This test checks that the config cache is invalidated in `execute_migration()`.
let t = TestContext::new().await;
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, false);
t.sql
.execute_migration(
"INSERT INTO config (keyname, value) VALUES ('is_chatmail', '1')",
1000,
)
.await?;
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, true);
assert_eq!(t.sql.get_raw_config_int(VERSION_CFG).await?.unwrap(), 1000);
Ok(())
self.set_db_version_in_cache(version).await
}
}

View File

@@ -83,7 +83,7 @@ impl InnerPool {
/// Retrieves a connection from the pool.
///
/// Sets `query_only` pragma to the provided value
/// to prevent accidental misuse of connection
/// to prevent accidentaly misuse of connection
/// for writing when reading is intended.
/// Only pass `query_only=false` if you want
/// to use the connection for writing.

View File

@@ -149,7 +149,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Message from %1$s"))]
SubjectForNewContact = 73,
/// Unused. Was used in group chat status messages.
#[strum(props(fallback = "Failed to send message to %1$s."))]
FailedSendingTo = 74,
@@ -431,9 +430,6 @@ pub enum StockMessage {
#[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
MsgReactedBy = 177,
#[strum(props(fallback = "Member %1$s removed."))]
MsgDelMember = 178,
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
SecurejoinWait = 190,
@@ -545,7 +541,7 @@ impl ContactId {
.unwrap_or_else(|_| self.to_string())
}
/// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set.
/// Get contact name, e.g. `Bob`, or `bob@exmple.net` if no name is set.
async fn get_stock_name(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
@@ -714,11 +710,7 @@ pub(crate) async fn msg_del_member_local(
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
if by_contact == ContactId::UNDEFINED {
translated(context, StockMessage::MsgDelMember)
.await
.replace1(whom)
} else if by_contact == ContactId::SELF {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouDelMember)
.await
.replace1(whom)
@@ -988,6 +980,13 @@ pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str)
.replace1(self_name)
}
/// Stock string: `Failed to send message to %1$s.`.
pub(crate) async fn failed_sending_to(context: &Context, name: &str) -> String {
translated(context, StockMessage::FailedSendingTo)
.await
.replace1(name)
}
/// Stock string: `Message deletion timer is disabled.`.
pub(crate) async fn msg_ephemeral_timer_disabled(
context: &Context,

View File

@@ -566,10 +566,6 @@ mod tests {
assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await?);
assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);
// Sync messages are "auto-generated", but they mustn't make the self-contact a bot.
let self_contact = alice2.add_or_lookup_contact(&alice2).await;
assert!(!self_contact.is_bot());
// the same sync message sent to bob must not be executed
let bob = TestContext::new_bob().await;
bob.recv_msg(&sent_msg).await;

View File

@@ -41,7 +41,6 @@ use crate::pgp::KeyPair;
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str::StockStrings;
use crate::tools::time;
#[allow(non_upper_case_globals)]
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
@@ -619,7 +618,7 @@ impl TestContext {
.filter(|msg| msg.chat_id != DC_CHAT_ID_TRASH)
}
/// Receives a message and asserts that it goes to trash chat.
/// Recevies a message and asserts that it goes to trash chat.
pub async fn recv_msg_trash(&self, msg: &SentMessage<'_>) {
let received = receive_imf(self, msg.payload().as_bytes(), false)
.await
@@ -881,7 +880,7 @@ impl TestContext {
let contact = self.add_or_lookup_contact(member).await;
to_add.push(contact.id);
}
add_to_chat_contacts_table(self, time(), chat_id, &to_add)
add_to_chat_contacts_table(self, chat_id, &to_add)
.await
.unwrap();
@@ -1352,24 +1351,6 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
.unwrap();
}
/// When dropped after a test failure,
/// prints a note about a possible false-possible caused by SystemTime::shift().
pub(crate) struct TimeShiftFalsePositiveNote;
impl Drop for TimeShiftFalsePositiveNote {
fn drop(&mut self) {
if std::thread::panicking() {
let green = nu_ansi_term::Color::Green.normal();
println!("{}", green.paint(
"\nNOTE: This test failure may be a false-positive, caused by tests running in parallel.
The issue is that `SystemTime::shift()` (a utility function for tests) changes the time for all threads doing tests, and not only for the running test.
Until the false-positive is fixed:
- Use `cargo test -- --test-threads 1` instead of `cargo test`
- Or use `cargo nextest run` (install with `cargo install cargo-nextest --locked`)\n")
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -371,7 +371,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
chat::add_contact_to_chat(&bob, group, bob_alice_contact).await?;
// Alice sends a message which Bob doesn't receive or something
// A real attack would rather reuse a message that was sent to a group
// A real attack would rather re-use a message that was sent to a group
// and replace the Message-Id or so.
let chat = alice.create_chat(&bob).await;
let sent = alice.send_text(chat.id, "whoop whoop").await;

View File

@@ -686,7 +686,7 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
assert_eq!(chats.len(), 1);
assert!(chats.len() == 1);
tcm.section("Bob reinstalls DC");
drop(bob);
@@ -709,7 +709,7 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
assert_eq!(chat.is_protected(), false);
assert_eq!(chat.is_protection_broken(), true);
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
assert_eq!(chats.len(), 1);
assert!(chats.len() == 1);
{
let alice_bob_chat = alice.get_chat(&bob_new).await;

View File

@@ -74,8 +74,8 @@ pub struct WebxdcManifest {
/// Optional URL of webxdc source code.
pub source_code_url: Option<String>,
/// Set to "map" to request integration.
pub request_integration: Option<String>,
/// If the webxdc requests network access.
pub request_internet_access: Option<bool>,
}
/// Parsed information from WebxdcManifest and fallbacks.
@@ -100,9 +100,6 @@ pub struct WebxdcInfo {
/// URL of webxdc source code or an empty string.
pub source_code_url: String,
/// Set to "map" to request integration, otherwise an empty string.
pub request_integration: String,
/// If the webxdc is allowed to access the network.
/// It should request access, be encrypted
/// and sent to self for this.
@@ -370,16 +367,18 @@ impl Context {
.get_overwritable_info_msg_id(&instance, from_id)
.await?;
if let (Some(info_msg_id), None) = (info_msg_id, &status_update_item.href) {
chat::update_msg_text_and_timestamp(
self,
instance.chat_id,
info_msg_id,
info.as_str(),
timestamp,
)
.await?;
notify_msg_id = info_msg_id;
if info_msg_id.is_some() && status_update_item.href.is_none() {
if let Some(info_msg_id) = info_msg_id {
chat::update_msg_text_and_timestamp(
self,
instance.chat_id,
info_msg_id,
info.as_str(),
timestamp,
)
.await?;
notify_msg_id = info_msg_id;
}
} else {
notify_msg_id = chat::add_info_msg_with_cmd(
self,
@@ -417,11 +416,15 @@ impl Context {
if from_id != ContactId::SELF {
if let Some(notify_list) = status_update_item.notify {
let self_addr = instance.get_webxdc_self_addr(self).await?;
if let Some(notify_text) =
notify_list.get(&self_addr).or_else(|| notify_list.get("*"))
{
if let Some(notify_text) = notify_list.get(&self_addr) {
self.emit_event(EventType::IncomingWebxdcNotify {
contact_id: from_id,
msg_id: notify_msg_id,
text: notify_text.clone(),
href: status_update_item.href,
});
} else if let Some(notify_text) = notify_list.get("*") {
self.emit_event(EventType::IncomingWebxdcNotify {
chat_id: instance.chat_id,
contact_id: from_id,
msg_id: notify_msg_id,
text: notify_text.clone(),
@@ -919,9 +922,9 @@ impl Message {
}
}
let request_integration = manifest.request_integration.unwrap_or_default();
let is_integrated = self.is_set_as_webxdc_integration(context).await?;
let internet_access = is_integrated;
let internet_access = manifest.request_internet_access.unwrap_or_default()
&& self.chat_id.is_self_talk(context).await.unwrap_or_default()
&& self.get_showpadlock();
let self_addr = self.get_webxdc_self_addr(context).await?;
@@ -943,11 +946,8 @@ impl Message {
.get(Param::WebxdcDocument)
.unwrap_or_default()
.to_string(),
summary: if is_integrated {
"🌍 Used as map. Delete to use default. Do not enter sensitive data".to_string()
} else if request_integration == "map" {
"🌏 To use as map, forward to \"Saved Messages\" again. Do not enter sensitive data"
.to_string()
summary: if internet_access {
"Dev Mode: Do not enter sensitive data!".to_string()
} else {
self.param
.get(Param::WebxdcSummary)
@@ -959,7 +959,6 @@ impl Message {
} else {
"".to_string()
},
request_integration,
internet_access,
self_addr,
send_update_interval: context.ratelimit.read().await.update_interval(),
@@ -2235,6 +2234,19 @@ sth_for_the = "future""#
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_webxdc_manifest_request_internet_access() -> Result<()> {
let result = parse_webxdc_manifest(r#"request_internet_access = 3"#.as_bytes());
assert!(result.is_err());
let manifest = parse_webxdc_manifest(r#" source_code_url="https://foo.org""#.as_bytes())?;
assert_eq!(manifest.request_internet_access, None);
let manifest = parse_webxdc_manifest(r#" request_internet_access=false"#.as_bytes())?;
assert_eq!(manifest.request_internet_access, Some(false));
let manifest = parse_webxdc_manifest(r#"request_internet_access = true"#.as_bytes())?;
assert_eq!(manifest.request_internet_access, Some(true));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_min_api_too_large() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -2645,16 +2657,16 @@ sth_for_the = "future""#
Ok(())
}
// check that `info.internet_access` is not set for normal, non-integrated webxdc -
// even if they use the deprecated option `request_internet_access` in manifest.toml
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_no_internet_access() -> Result<()> {
async fn test_webxdc_internet_access() -> Result<()> {
let t = TestContext::new_alice().await;
let self_id = t.get_self_chat().await.id;
let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id;
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
let broadcast_id = create_broadcast_list(&t).await?;
let mut first_test = true; // only the first test has all conditions for internet access
for e2ee in ["1", "0"] {
t.set_config(Config::E2eeEnabled, Some(e2ee)).await?;
for chat_id in [self_id, single_id, group_id, broadcast_id] {
@@ -2677,7 +2689,11 @@ sth_for_the = "future""#
.await?;
let instance = Message::load_from_db(&t, instance_id).await?;
let info = instance.get_webxdc_info(&t).await?;
assert_eq!(info.internet_access, false);
assert_eq!(info.internet_access, first_test);
assert_eq!(info.summary.contains("Do not enter sensitive"), first_test);
assert_eq!(info.summary.contains("real summary"), !first_test);
first_test = false;
}
}
}

View File

@@ -57,34 +57,14 @@ impl Context {
}
// Check if a Webxdc shall be used as an integration and remember that.
pub(crate) async fn update_webxdc_integration_database(
&self,
msg: &mut Message,
context: &Context,
) -> Result<()> {
if msg.viewtype == Viewtype::Webxdc {
let is_integration = if msg.param.get_int(Param::WebxdcIntegration).is_some() {
true
} else if msg.chat_id.is_self_talk(context).await? {
let info = msg.get_webxdc_info(context).await?;
if info.request_integration == "map" {
msg.param.set_int(Param::WebxdcIntegration, 1);
msg.update_param(context).await?;
true
} else {
false
}
} else {
false
};
if is_integration {
self.set_config_internal(
Config::WebxdcIntegration,
Some(&msg.id.to_u32().to_string()),
)
.await?;
}
pub(crate) async fn update_webxdc_integration_database(&self, msg: &Message) -> Result<()> {
if msg.viewtype == Viewtype::Webxdc && msg.param.get_int(Param::WebxdcIntegration).is_some()
{
self.set_config_internal(
Config::WebxdcIntegration,
Some(&msg.id.to_u32().to_string()),
)
.await?;
}
Ok(())
}
@@ -121,26 +101,11 @@ impl Message {
None
}
}
// Check if the message is an actually used as Webxdc integration.
pub(crate) async fn is_set_as_webxdc_integration(&self, context: &Context) -> Result<bool> {
if let Some(integration_id) = context
.get_config_parsed::<u32>(Config::WebxdcIntegration)
.await?
{
Ok(integration_id == self.id.to_u32())
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use crate::config::Config;
use crate::context::Context;
use crate::message;
use crate::message::{Message, Viewtype};
use crate::test_utils::TestContext;
use anyhow::Result;
use std::time::Duration;
@@ -161,65 +126,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_overwrite_default_integration() -> Result<()> {
let t = TestContext::new_alice().await;
let self_chat = &t.get_self_chat().await;
assert!(t.init_webxdc_integration(None).await?.is_none());
async fn assert_integration(t: &Context, name: &str) -> Result<()> {
let integration_id = t.init_webxdc_integration(None).await?.unwrap();
let integration = Message::load_from_db(t, integration_id).await?;
let integration_info = integration.get_webxdc_info(t).await?;
assert_eq!(integration_info.name, name);
Ok(())
}
// set default integration
let bytes = include_bytes!("../../test-data/webxdc/with-manifest-and-png-icon.xdc");
let file = t.get_blobdir().join("maps.xdc");
tokio::fs::write(&file, bytes).await.unwrap();
t.set_webxdc_integration(file.to_str().unwrap()).await?;
assert_integration(&t, "with some icon").await?;
// send a maps.xdc with insufficient manifest
let mut msg = Message::new(Viewtype::Webxdc);
msg.set_file_from_bytes(
&t,
"mapstest.xdc",
include_bytes!("../../test-data/webxdc/mapstest-integration-unset.xdc"),
None,
)
.await?;
t.send_msg(self_chat.id, &mut msg).await;
assert_integration(&t, "with some icon").await?; // still the default integration
// send a maps.xdc with manifest including the line `request_integration = "map"`
let mut msg = Message::new(Viewtype::Webxdc);
msg.set_file_from_bytes(
&t,
"mapstest.xdc",
include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc"),
None,
)
.await?;
let sent = t.send_msg(self_chat.id, &mut msg).await;
let info = msg.get_webxdc_info(&t).await?;
assert!(info.summary.contains("Used as map"));
assert_integration(&t, "Maps Test 2").await?;
// when maps.xdc is received on another device, the integration is not accepted (needs to be forwarded again)
let t2 = TestContext::new_alice().await;
let msg2 = t2.recv_msg(&sent).await;
let info = msg2.get_webxdc_info(&t2).await?;
assert!(info.summary.contains("To use as map,"));
assert!(t2.init_webxdc_integration(None).await?.is_none());
// deleting maps.xdc removes the user's integration - the UI will go back to default calling set_webxdc_integration() then
message::delete_msgs(&t, &[msg.id]).await?;
assert!(t.init_webxdc_integration(None).await?.is_none());
Ok(())
}
}

View File

@@ -181,7 +181,7 @@ mod tests {
async fn test_maps_integration() -> Result<()> {
let t = TestContext::new_alice().await;
let bytes = include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc");
let bytes = include_bytes!("../../test-data/webxdc/mapstest.xdc");
let file = t.get_blobdir().join("maps.xdc");
tokio::fs::write(&file, bytes).await.unwrap();
t.set_webxdc_integration(file.to_str().unwrap()).await?;
@@ -199,7 +199,7 @@ mod tests {
let integration = Message::load_from_db(&t, integration_id).await?;
let info = integration.get_webxdc_info(&t).await?;
assert_eq!(info.name, "Maps Test 2");
assert_eq!(info.name, "Maps Test");
assert_eq!(info.internet_access, true);
t.send_webxdc_status_update(

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