mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 23:52:11 +03:00
Compare commits
1 Commits
v1.155.4
...
hoc/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61fb9e7e3b |
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.84.1
|
||||
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,15 +95,15 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.84.1
|
||||
rust: 1.82.0
|
||||
- os: windows-latest
|
||||
rust: 1.84.1
|
||||
rust: 1.82.0
|
||||
- os: macos-latest
|
||||
rust: 1.84.1
|
||||
rust: 1.82.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.81.0
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.77.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -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:
|
||||
|
||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2.3.0
|
||||
uses: dependabot/fetch-metadata@v2.2.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve a PR
|
||||
|
||||
424
CHANGELOG.md
424
CHANGELOG.md
@@ -1,406 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.155.4] - 2025-02-10
|
||||
|
||||
### CI
|
||||
|
||||
- Upgrade Rust from 1.84.0 to 1.84.1.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Use CRLF newlines in vCards.
|
||||
- Make vCard parsing more robust in case of trailing newlines.
|
||||
- Do not include CRLF before MIME boundary in the part body.
|
||||
- Accept QR codes with 'broken' JSON ([#6528](https://github.com/deltachat/deltachat-core-rust/pull/6528)).
|
||||
|
||||
### Other
|
||||
|
||||
- Add `MessageQuote.chat_id`.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move even more tests into their own files ([#6521](https://github.com/deltachat/deltachat-core-rust/pull/6521)).
|
||||
|
||||
## [1.155.3] - 2025-02-05
|
||||
|
||||
### Fixes
|
||||
|
||||
- Store device token in IMAP METADATA on each connection.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Upgrade iroh from 0.30 to 0.32.
|
||||
- Update `pgp` to 0.15.
|
||||
- cargo: Bump thiserror from 1.0.69 to 2.0.9.
|
||||
- cargo: Bump pin-project from 1.1.7 to 1.1.8.
|
||||
- cargo: Bump dirs from 5.0.1 to 6.0.0.
|
||||
- cargo: Bump hyper from 1.5.2 to 1.6.0.
|
||||
- cargo: Bump webpki-roots from 0.26.7 to 0.26.8.
|
||||
- cargo: Bump futures-lite from 2.5.0 to 2.6.0.
|
||||
- Update OpenSSL to fix RUSTSEC-2025-0004.
|
||||
- cargo: Bump tokio from 1.42.0 to 1.43.0.
|
||||
- cargo: Bump syn from 2.0.94 to 2.0.98.
|
||||
- cargo: Bump rustls from 0.23.20 to 0.23.22.
|
||||
- cargo: Bump data-encoding from 2.6.0 to 2.7.0.
|
||||
- cargo: Bump serde_json from 1.0.134 to 1.0.138.
|
||||
- cargo: Bump uuid from 1.11.0 to 1.12.1.
|
||||
- cargo: Bump log from 0.4.22 to 0.4.25.
|
||||
- cargo: Bump rustls-pki-types from 1.10.1 to 1.11.0.
|
||||
- Update futures-concurrency.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Assign docs to correct object.
|
||||
|
||||
### Tests
|
||||
|
||||
- Make sure DCBACKUP2 compatibility does not break again.
|
||||
|
||||
## [1.155.2] - 2025-01-31
|
||||
|
||||
This release accidentally broke compatibility
|
||||
with previous versions of `DCBACKUP2` QR codes
|
||||
due to iroh upgrade.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `IncomingReaction.chat_id` ([#6459](https://github.com/deltachat/deltachat-core-rust/pull/6459)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Deduplicate blob files in `chat.rs`, `config.rs`, and `integration.rs`.
|
||||
- Improve logging around IMAP IDLE.
|
||||
- Upgrade to iroh@0.30.0.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't remove file extension when recoding avatars.
|
||||
- Use `BufReader` when reading .xdc files.
|
||||
- No implicit member changes when we are added to the group ([#6493](https://github.com/deltachat/deltachat-core-rust/pull/6493)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- jsonrpc: Update documentation for `select_account` and `get_selected_account_id` ([#6483](https://github.com/deltachat/deltachat-core-rust/pull/6483)).
|
||||
- jsonrpc: Add docs for some functions.
|
||||
|
||||
## [1.155.1] - 2025-01-25
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Only accept SetContacts sync messages for broadcast lists.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't create tombstones when synchronizing broadcast list members.
|
||||
- Use non-empty `To:` field for "saved messages".
|
||||
- Only send Chat-Group-Member-Timestamps in groups.
|
||||
- Use 0 timestamps if Chat-Group-Member-Timestamps is not set.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove BlobObject::create(), use create_and_deduplicate_from_bytes() instead ([#6467](https://github.com/deltachat/deltachat-core-rust/pull/6467)).
|
||||
- Move more tests into their own files ([#6473](https://github.com/deltachat/deltachat-core-rust/pull/6473)).
|
||||
|
||||
## [1.155.0] - 2025-01-23
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC API to get past members.
|
||||
|
||||
### Build system
|
||||
|
||||
- Update Rust.
|
||||
- Increase MSRV to 1.81.0
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- feat: Set BccSelf to true when receiving a sync message ([#6434](https://github.com/deltachat/deltachat-core-rust/pull/6434))
|
||||
- File deduplication ([#6332](https://github.com/deltachat/deltachat-core-rust/pull/6332))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move tests to their own files.
|
||||
- Extract `group_changes_msgs()` function ([#6460](https://github.com/deltachat/deltachat-core-rust/pull/6460)).
|
||||
|
||||
## [1.154.3] - 2025-01-20
|
||||
|
||||
### Build system
|
||||
|
||||
- Remove encoded-words from flake.nix.
|
||||
- nix: Update rust-email hash in flake.nix.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Remove unused function delete_files_in_dir() ([#6454](https://github.com/deltachat/deltachat-core-rust/pull/6454)).
|
||||
|
||||
## [1.154.2] - 2025-01-20
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add API to save messages ([#5606](https://github.com/deltachat/deltachat-core-rust/pull/5606)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- fix: Don't accidentally remove Self from groups ([#6455](https://github.com/deltachat/deltachat-core-rust/pull/6455)).
|
||||
- Do not create tombstones for members removed from unpromoted groups.
|
||||
|
||||
### Build system
|
||||
|
||||
- Switch to non-git version of encoded-words.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Make memberlist update logic easier to follow.
|
||||
|
||||
## [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
|
||||
@@ -773,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
|
||||
|
||||
@@ -1364,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
|
||||
|
||||
@@ -2301,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)).
|
||||
@@ -5770,20 +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
|
||||
[1.154.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.1..v1.154.2
|
||||
[1.154.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.2..v1.154.3
|
||||
[1.155.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.3..v1.155.0
|
||||
[1.155.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.0..v1.155.1
|
||||
[1.155.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.1..v1.155.2
|
||||
[1.155.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.2..v1.155.3
|
||||
[1.155.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.3..v1.155.4
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
1987
Cargo.lock
generated
1987
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
53
Cargo.toml
53
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.155.4"
|
||||
version = "1.151.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.81"
|
||||
rust-version = "1.77"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[profile.dev]
|
||||
@@ -39,37 +39,36 @@ 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"] }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
data-encoding = "2.7.0"
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = "0.2"
|
||||
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 }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "=0.25.0-alpha.4"
|
||||
hickory-resolver = "=0.25.0-alpha.2"
|
||||
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"] }
|
||||
iroh-gossip = { version = "0.32", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.32", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
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.0"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = { workspace = true }
|
||||
mailparse = "0.16"
|
||||
mailparse = "0.15"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
@@ -77,7 +76,7 @@ num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.15.0", default-features = false }
|
||||
pgp = { version = "0.14.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
@@ -86,15 +85,15 @@ rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.11.0"
|
||||
rustls = { version = "0.23.22", 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"
|
||||
@@ -102,16 +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.8"
|
||||
blake3 = "1.5.5"
|
||||
webpki-roots = "0.26.6"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -122,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]
|
||||
@@ -152,7 +150,6 @@ harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "receive_emails"
|
||||
required-features = ["internals"]
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
@@ -171,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.6.0"
|
||||
futures-lite = "2.4.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
nu-ansi-term = "0.46"
|
||||
@@ -188,10 +185,10 @@ rusqlite = "0.32"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.14.0"
|
||||
thiserror = "2"
|
||||
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"
|
||||
|
||||
|
||||
@@ -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/)\]
|
||||
|
||||
@@ -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
80
contrib/proxy.py
Normal 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()
|
||||
@@ -78,21 +78,21 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
let addr = &c.addr;
|
||||
let display_name = c.display_name();
|
||||
res += &format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:{addr}\r\n\
|
||||
FN:{display_name}\r\n"
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:{addr}\n\
|
||||
FN:{display_name}\n"
|
||||
);
|
||||
if let Some(key) = &c.key {
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\n");
|
||||
}
|
||||
if let Some(profile_image) = &c.profile_image {
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n");
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\r\n");
|
||||
res += &format!("REV:{timestamp}\n");
|
||||
}
|
||||
res += "END:VCARD\r\n";
|
||||
res += "END:VCARD\n";
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -206,21 +206,22 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
} else if let Some(rev) = vcard_property(line, "rev") {
|
||||
datetime.get_or_insert(rev);
|
||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||
let (authname, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
|
||||
contacts.push(VcardContact {
|
||||
authname,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
timestamp: datetime
|
||||
.context("No timestamp in vcard")
|
||||
.and_then(parse_datetime),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (authname, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
|
||||
contacts.push(VcardContact {
|
||||
authname,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
timestamp: datetime
|
||||
.context("No timestamp in vcard")
|
||||
.and_then(parse_datetime),
|
||||
});
|
||||
}
|
||||
|
||||
contacts
|
||||
@@ -539,30 +540,6 @@ END:VCARD",
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_with_trailing_newline() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD\r
|
||||
VERSION:4.0\r
|
||||
FN:Alice Wonderland\r
|
||||
N:Wonderland;Alice;;;Ms.\r
|
||||
GENDER:W\r
|
||||
EMAIL;TYPE=work:alice@example.com\r
|
||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]\r
|
||||
REV:20240418T184242Z\r
|
||||
END:VCARD\r
|
||||
\r",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_and_parse_vcard() {
|
||||
let contacts = [
|
||||
@@ -582,20 +559,20 @@ END:VCARD\r
|
||||
},
|
||||
];
|
||||
let items = [
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:alice@example.org\r\n\
|
||||
FN:Alice Wonderland\r\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
|
||||
REV:20240418T184242Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:bob@example.com\r\n\
|
||||
FN:bob@example.com\r\n\
|
||||
REV:19700101T000000Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:alice@example.org\n\
|
||||
FN:Alice Wonderland\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\n\
|
||||
REV:20240418T184242Z\n\
|
||||
END:VCARD\n",
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:bob@example.com\n\
|
||||
FN:bob@example.com\n\
|
||||
REV:19700101T000000Z\n\
|
||||
END:VCARD\n",
|
||||
];
|
||||
let mut expected = "".to_string();
|
||||
for len in 0..=contacts.len() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.155.4"
|
||||
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"]
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -1974,36 +2031,6 @@ void dc_delete_msgs (dc_context_t* context, const uint3
|
||||
void dc_forward_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a copy of messages in "Saved Messages".
|
||||
*
|
||||
* In contrast to forwarding messages,
|
||||
* information as author, date and origin are preserved.
|
||||
* The action completes locally, so "Saved Messages" do not show sending errors in case one is offline.
|
||||
* Still, a sync message is emitted, so that other devices will save the same message,
|
||||
* as long as not deleted before.
|
||||
*
|
||||
* To check if a message was saved, use dc_msg_get_saved_msg_id(),
|
||||
* UI may show an indicator and offer an "Unsave" instead of a "Save" button then.
|
||||
*
|
||||
* The other way round, from inside the "Saved Messages" chat,
|
||||
* UI may show the indicator and "Unsave" button checking dc_msg_get_original_msg_id()
|
||||
* and offer a button to go the original message.
|
||||
*
|
||||
* "Unsave" is done by deleting the saved message.
|
||||
* Webxdc updates are not copied on purpose.
|
||||
*
|
||||
* For performance reasons, esp. when saving lots of messages,
|
||||
* UI should call this function from a background thread.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_ids An array of uint32_t containing all message IDs that should be saved.
|
||||
* @param msg_cnt The number of messages IDs in the msg_ids array.
|
||||
*/
|
||||
void dc_save_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
|
||||
/**
|
||||
* Resend messages and make information available for newly added chat members.
|
||||
* Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||
@@ -3964,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).
|
||||
@@ -4174,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.
|
||||
@@ -4514,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.
|
||||
@@ -4665,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.
|
||||
*
|
||||
@@ -4752,36 +4794,10 @@ void dc_msg_set_override_sender_name(dc_msg_t* msg, const char* name)
|
||||
* @param file If the message object is used in dc_send_msg() later,
|
||||
* this must be the full path of the image file to send.
|
||||
* @param filemime The MIME type of the file. NULL if you don't know or don't care.
|
||||
* @deprecated 2025-01-21 Use dc_msg_set_file_and_deduplicate instead
|
||||
*/
|
||||
void dc_msg_set_file (dc_msg_t* msg, const char* file, const char* filemime);
|
||||
|
||||
|
||||
/**
|
||||
* Sets the file associated with a message.
|
||||
*
|
||||
* If `name` is non-null, it is used as the file name
|
||||
* and the actual current name of the file is ignored.
|
||||
*
|
||||
* If the source file is already in the blobdir, it will be renamed,
|
||||
* otherwise it will be copied to the blobdir first.
|
||||
*
|
||||
* In order to deduplicate files that contain the same data,
|
||||
* the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
|
||||
*
|
||||
* NOTE:
|
||||
* - This function will rename the file. To get the new file path, call `get_file()`.
|
||||
* - The file must not be modified after this function was called.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object. Must not be NULL.
|
||||
* @param file The path of the file to attach. Must not be NULL.
|
||||
* @param name The original filename of the attachment. If NULL, the current name of `file` will be used instead.
|
||||
* @param filemime The MIME type of the file. NULL if you don't know or don't care.
|
||||
*/
|
||||
void dc_msg_set_file_and_deduplicate(dc_msg_t* msg, const char* file, const char* name, const char* filemime);
|
||||
|
||||
|
||||
/**
|
||||
* Set the dimensions associated with message object.
|
||||
* Typically this is the width and the height of an image or video associated using dc_msg_set_file().
|
||||
@@ -4924,35 +4940,6 @@ dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
|
||||
dc_msg_t* dc_msg_get_parent (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get original message ID for a saved message from the "Saved Messages" chat.
|
||||
*
|
||||
* Can be used by UI to show a button to go the original message
|
||||
* and an option to "Unsave" the message.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object. Usually, this refers to a a message inside "Saved Messages".
|
||||
* @return The message ID of the original message.
|
||||
* 0 if the given message object is not a "Saved Message"
|
||||
* or if the original message does no longer exist.
|
||||
*/
|
||||
uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a message was saved and return its ID inside "Saved Messages".
|
||||
*
|
||||
* Deleting the returned message will un-save the message.
|
||||
* The state "is saved" can be used to show some icon to indicate that a message was saved.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object. Usually, this refers to a a message outside "Saved Messages".
|
||||
* @return The message ID inside "Saved Messages", if any.
|
||||
* 0 if the given message object is not saved.
|
||||
*/
|
||||
uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Force the message to be sent in plain text.
|
||||
*
|
||||
@@ -5478,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.
|
||||
*/
|
||||
@@ -5584,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
|
||||
|
||||
@@ -5806,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
|
||||
@@ -5843,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.
|
||||
@@ -6935,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
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -1979,26 +2008,6 @@ pub unsafe extern "C" fn dc_forward_msgs(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_save_msgs(
|
||||
context: *mut dc_context_t,
|
||||
msg_ids: *const u32,
|
||||
msg_cnt: libc::c_int,
|
||||
) {
|
||||
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
|
||||
eprintln!("ignoring careless call to dc_save_msgs()");
|
||||
return;
|
||||
}
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::save_msgs(ctx, &msg_ids[..])
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed to save message")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_resend_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -3714,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() {
|
||||
@@ -3835,33 +3854,6 @@ pub unsafe extern "C" fn dc_msg_set_file(
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_set_file_and_deduplicate(
|
||||
msg: *mut dc_msg_t,
|
||||
file: *const libc::c_char,
|
||||
name: *const libc::c_char,
|
||||
filemime: *const libc::c_char,
|
||||
) {
|
||||
if msg.is_null() || file.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_set_file_and_deduplicate()");
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
ffi_msg
|
||||
.message
|
||||
.set_file_and_deduplicate(
|
||||
ctx,
|
||||
as_path(file),
|
||||
to_opt_string_lossy(name).as_deref(),
|
||||
to_opt_string_lossy(filemime).as_deref(),
|
||||
)
|
||||
.context("Failed to set file")
|
||||
.log_err(&*ffi_msg.context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_set_dimension(
|
||||
msg: *mut dc_msg_t,
|
||||
@@ -4027,48 +4019,6 @@ pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_original_msg_id(msg: *const dc_msg_t) -> u32 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_original_msg_id()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg: &MessageWrapper = &*msg;
|
||||
let context = &*ffi_msg.context;
|
||||
block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.get_original_msg_id(context)
|
||||
.await
|
||||
.context("failed to get original message")
|
||||
.log_err(context)
|
||||
.unwrap_or_default()
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_saved_msg_id(msg: *const dc_msg_t) -> u32 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_saved_msg_id()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg: &MessageWrapper = &*msg;
|
||||
let context = &*ffi_msg.context;
|
||||
block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.get_saved_msg_id(context)
|
||||
.await
|
||||
.context("failed to get original message")
|
||||
.log_err(context)
|
||||
.unwrap_or_default()
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
|
||||
if msg.is_null() {
|
||||
@@ -5021,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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.155.4"
|
||||
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"] }
|
||||
|
||||
@@ -212,12 +212,14 @@ impl CommandApi {
|
||||
self.accounts.read().await.get_all()
|
||||
}
|
||||
|
||||
/// Select account in account manager, this saves the last used account to accounts.toml
|
||||
/// Select account id for internally selected state.
|
||||
/// TODO: Likely this is deprecated as all methods take an account id now.
|
||||
async fn select_account(&self, id: u32) -> Result<()> {
|
||||
self.accounts.write().await.select_account(id).await
|
||||
}
|
||||
|
||||
/// Get the selected account from the account manager (on startup it is read from accounts.toml)
|
||||
/// Get the selected account id of the internal state..
|
||||
/// TODO: Likely this is deprecated as all methods take an account id now.
|
||||
async fn get_selected_account_id(&self) -> Option<u32> {
|
||||
self.accounts.read().await.get_selected_account_id()
|
||||
}
|
||||
@@ -834,13 +836,6 @@ impl CommandApi {
|
||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Returns contact IDs of the past chat members.
|
||||
async fn get_past_chat_contacts(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contacts = chat::get_past_chat_contacts(&ctx, ChatId::new(chat_id)).await?;
|
||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Create a new group chat.
|
||||
///
|
||||
/// After creation,
|
||||
@@ -998,12 +993,6 @@ impl CommandApi {
|
||||
marknoticed_chat(&ctx, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Returns the message that is immediately followed by the last seen
|
||||
/// message.
|
||||
/// From the point of view of the user this is effectively
|
||||
/// "first unread", but in reality in the database a seen message
|
||||
/// _can_ be followed by a fresh (unseen) message
|
||||
/// if that message has not been individually marked as seen.
|
||||
async fn get_first_unread_message_of_chat(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1092,9 +1081,6 @@ impl CommandApi {
|
||||
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
|
||||
}
|
||||
|
||||
/// Returns all messages of a particular chat.
|
||||
/// If `add_daymarker` is `true`, it will return them as
|
||||
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
|
||||
async fn get_message_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1781,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)
|
||||
@@ -1843,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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
|
||||
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
@@ -39,10 +39,6 @@ pub struct FullChat {
|
||||
is_self_talk: bool,
|
||||
contacts: Vec<ContactObject>,
|
||||
contact_ids: Vec<u32>,
|
||||
|
||||
/// Contact IDs of the past chat members.
|
||||
past_contact_ids: Vec<u32>,
|
||||
|
||||
color: String,
|
||||
fresh_message_counter: usize,
|
||||
// is_group - please check over chat.type in frontend instead
|
||||
@@ -63,7 +59,6 @@ impl FullChat {
|
||||
let chat = Chat::load_from_db(context, rust_chat_id).await?;
|
||||
|
||||
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
||||
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
||||
|
||||
let mut contacts = Vec::with_capacity(contact_ids.len());
|
||||
|
||||
@@ -116,7 +111,6 @@ impl FullChat {
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
contacts,
|
||||
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||
color,
|
||||
fresh_message_counter,
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
|
||||
@@ -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.
|
||||
@@ -101,7 +101,6 @@ pub enum EventType {
|
||||
/// Incoming reaction, should be notified.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingReaction {
|
||||
chat_id: u32,
|
||||
contact_id: u32,
|
||||
msg_id: u32,
|
||||
reaction: String,
|
||||
@@ -110,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,
|
||||
@@ -336,24 +334,20 @@ impl From<CoreEventType> for EventType {
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingReaction {
|
||||
chat_id,
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
} => IncomingReaction {
|
||||
chat_id: chat_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
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,
|
||||
|
||||
@@ -85,8 +85,6 @@ pub struct MessageObject {
|
||||
|
||||
webxdc_info: Option<WebxdcMessageInfo>,
|
||||
|
||||
webxdc_href: Option<String>,
|
||||
|
||||
download_state: DownloadState,
|
||||
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
@@ -104,9 +102,6 @@ enum MessageQuote {
|
||||
WithMessage {
|
||||
text: String,
|
||||
message_id: u32,
|
||||
/// The quoted message does not always belong
|
||||
/// to the same chat, e.g. when "Reply Privately" is used.
|
||||
chat_id: u32,
|
||||
author_display_name: String,
|
||||
author_display_color: String,
|
||||
override_sender_name: Option<String>,
|
||||
@@ -150,7 +145,6 @@ impl MessageObject {
|
||||
Some(MessageQuote::WithMessage {
|
||||
text: quoted_text,
|
||||
message_id: quote.get_id().to_u32(),
|
||||
chat_id: quote.get_chat_id().to_u32(),
|
||||
author_display_name: quote_author.get_display_name().to_owned(),
|
||||
author_display_color: color_int_to_hex_string(quote_author.get_color()),
|
||||
override_sender_name: quote.get_override_sender_name(),
|
||||
@@ -247,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,
|
||||
@@ -277,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,
|
||||
|
||||
@@ -57,7 +57,6 @@ impl WebxdcMessageInfo {
|
||||
document,
|
||||
summary,
|
||||
source_code_url,
|
||||
request_integration: _,
|
||||
internet_access,
|
||||
self_addr,
|
||||
send_update_interval,
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.155.4"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.155.4"
|
||||
version = "1.151.2"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
@@ -8,12 +8,12 @@ repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true, features = ["internals"]}
|
||||
dirs = "6"
|
||||
dirs = "5"
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "15"
|
||||
rustyline = "14"
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
|
||||
@@ -939,7 +939,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
} else {
|
||||
Viewtype::File
|
||||
});
|
||||
msg.set_file_and_deduplicate(&context, Path::new(arg1), None, None)?;
|
||||
msg.set_file(arg1, None);
|
||||
msg.set_text(arg2.to_string());
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::highlight::{CmdKind as HighlightCmdKind, Highlighter, MatchingBracketHighlighter};
|
||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||
use rustyline::hint::{Hinter, HistoryHinter};
|
||||
use rustyline::validate::Validator;
|
||||
use rustyline::{
|
||||
@@ -298,8 +298,8 @@ impl Highlighter for DcHelper {
|
||||
self.highlighter.highlight(line, pos)
|
||||
}
|
||||
|
||||
fn highlight_char(&self, line: &str, pos: usize, kind: HighlightCmdKind) -> bool {
|
||||
self.highlighter.highlight_char(line, pos, kind)
|
||||
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
|
||||
self.highlighter.highlight_char(line, pos, forced)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.155.4"
|
||||
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 = [
|
||||
|
||||
@@ -238,11 +238,6 @@ class Chat:
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
def get_past_contacts(self) -> list[Contact]:
|
||||
"""Get past contacts for this chat."""
|
||||
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in past_contacts]
|
||||
|
||||
def set_image(self, path: str) -> None:
|
||||
"""Set profile image of this chat.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -231,9 +231,7 @@ def test_chat(acfactory) -> None:
|
||||
group.get_fresh_message_count()
|
||||
group.mark_noticed()
|
||||
assert group.get_contacts()
|
||||
assert group.get_past_contacts() == []
|
||||
group.remove_contact(alice_contact_bob)
|
||||
assert len(group.get_past_contacts()) == 1
|
||||
group.remove_contact(alice_chat_bob)
|
||||
group.get_locations()
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.155.4"
|
||||
version = "1.151.2"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.155.4"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
55
deny.toml
55
deny.toml
@@ -12,6 +12,10 @@ ignore = [
|
||||
# Unmaintained encoding
|
||||
"RUSTSEC-2021-0153",
|
||||
|
||||
# Unmaintained proc-macro-error
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2024-0370>
|
||||
"RUSTSEC-2024-0370",
|
||||
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
]
|
||||
@@ -26,15 +30,17 @@ skip = [
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "core-foundation", version = "0.9.4" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "generator", version = "0.7.5" },
|
||||
{ name = "event-listener", version = "4.0.3" },
|
||||
{ name = "fastrand", version = "1.9.0" },
|
||||
{ name = "fiat-crypto", version = "0.1.20" },
|
||||
{ name = "futures-lite", version = "1.13.0" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "nix", version = "0.27.1" },
|
||||
{ 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" },
|
||||
@@ -42,33 +48,24 @@ skip = [
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rtnetlink", version = "0.13.1" },
|
||||
{ name = "security-framework", version = "2.11.1" },
|
||||
{ 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 = "tokio-tungstenite", version = "0.21.0" },
|
||||
{ name = "tungstenite", version = "0.21.0" },
|
||||
{ name = "unicode-width", version = "0.1.11" },
|
||||
{ name = "toml_edit", version = "0.19.15" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
{ name = "windows_aarch64_msvc" },
|
||||
{ name = "windows-core" },
|
||||
{ name = "windows_i686_gnu" },
|
||||
{ name = "windows_i686_gnullvm" },
|
||||
{ name = "windows_i686_msvc" },
|
||||
{ name = "windows-implement" },
|
||||
{ name = "windows-interface" },
|
||||
{ name = "windows-result" },
|
||||
{ name = "windows-strings" },
|
||||
{ name = "windows-sys" },
|
||||
{ name = "windows-targets" },
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.52" },
|
||||
{ name = "windows-core", version = "<0.54.0" },
|
||||
{ name = "windows_i686_gnu", version = "<0.52" },
|
||||
{ name = "windows_i686_msvc", version = "<0.52" },
|
||||
{ name = "windows-sys", version = "<0.59" },
|
||||
{ name = "windows-targets", version = "<0.52" },
|
||||
{ name = "windows", version = "<0.54.0" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
|
||||
@@ -84,7 +81,6 @@ allow = [
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
]
|
||||
@@ -99,5 +95,6 @@ license-files = [
|
||||
[sources.allow-org]
|
||||
# Organisations which we allow git sources from.
|
||||
github = [
|
||||
"async-email",
|
||||
"deltachat",
|
||||
]
|
||||
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -47,11 +47,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1737527504,
|
||||
"narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=",
|
||||
"lastModified": 1731393059,
|
||||
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d",
|
||||
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -147,11 +147,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1737469691,
|
||||
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
|
||||
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -202,11 +202,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1737453499,
|
||||
"narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=",
|
||||
"lastModified": 1731342671,
|
||||
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "0b68402d781955d526b80e5d479e9e47addb4075",
|
||||
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -88,7 +88,8 @@
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"email-0.0.20" = "sha256-cfR3D5jFQpw32bGsgapK2Uwuxmht+rRK/n1ZUmCb2WA=";
|
||||
"email-0.0.20" = "sha256-rV4Uzqt2Qdrfi5Ti1r+Si1c2iW1kKyWLwOgLkQ5JGGw=";
|
||||
"encoded-words-0.2.0" = "sha256-KK9st0hLFh4dsrnLd6D8lC6pRFFs8W+WpZSGMGJcosk=";
|
||||
"lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk=";
|
||||
};
|
||||
};
|
||||
|
||||
270
fuzz/Cargo.lock
generated
270
fuzz/Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const buildArgs = [
|
||||
'build',
|
||||
'--release',
|
||||
'--features',
|
||||
'vendored',
|
||||
'vendored,jsonrpc',
|
||||
'-p',
|
||||
'deltachat_ffi'
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { EventId2EventName, C } from '../dist/constants.js'
|
||||
import { join } from 'path'
|
||||
import { statSync } from 'fs'
|
||||
import { Context } from '../dist/context.js'
|
||||
import { fileURLToPath } from 'url';
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
@@ -444,7 +444,7 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
context.setChatProfileImage(chatId, imagePath)
|
||||
const blobPath = context.getChat(chatId).getProfileImage()
|
||||
expect(blobPath.startsWith(blobs)).to.be.true
|
||||
expect(blobPath.includes('image')).to.be.false
|
||||
expect(blobPath.includes('image')).to.be.true
|
||||
expect(blobPath.endsWith('.jpeg')).to.be.true
|
||||
|
||||
context.setChatProfileImage(chatId, null)
|
||||
@@ -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')
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.155.4"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -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::
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.155.4"
|
||||
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" },
|
||||
]
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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}).*")
|
||||
|
||||
@@ -108,9 +108,7 @@ class Message:
|
||||
|
||||
@props.with_doc
|
||||
def filename(self):
|
||||
"""file path if there was an attachment, otherwise empty string.
|
||||
If you want to get the file extension or a user-visible string,
|
||||
use `basename` instead."""
|
||||
"""filename if there was an attachment, otherwise empty string."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
|
||||
|
||||
def set_file(self, path, mime_type=None):
|
||||
@@ -122,8 +120,7 @@ class Message:
|
||||
|
||||
@props.with_doc
|
||||
def basename(self) -> str:
|
||||
"""The user-visible name of the attachment (incl. extension)
|
||||
if it exists, otherwise empty string."""
|
||||
"""basename of the attachment if it exists, otherwise empty string."""
|
||||
# FIXME, it does not return basename
|
||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -181,16 +181,15 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
|
||||
msg = send_and_receive_message()
|
||||
assert msg.text == "withfile"
|
||||
assert open(msg.filename).read() == "some data"
|
||||
msg.basename.index(basename)
|
||||
assert msg.basename.endswith(ext)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
msg2 = send_and_receive_message()
|
||||
assert msg2.text == "withfile"
|
||||
assert open(msg2.filename).read() == "some data"
|
||||
msg2.basename.index(basename)
|
||||
assert msg2.basename.endswith(ext)
|
||||
assert msg.filename == msg2.filename # The file is deduplicated
|
||||
assert msg.basename == msg2.basename
|
||||
msg2.filename.index(basename)
|
||||
assert msg2.filename.endswith(ext)
|
||||
assert msg.filename != msg2.filename
|
||||
|
||||
|
||||
def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||
@@ -215,8 +214,8 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
assert open(msg.filename).read() == content
|
||||
msg.basename.index(basename)
|
||||
assert msg.basename.endswith(ext)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
|
||||
def test_html_message(acfactory, lp):
|
||||
@@ -1254,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")
|
||||
@@ -1280,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")
|
||||
@@ -1297,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")
|
||||
@@ -1371,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
|
||||
@@ -1903,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",
|
||||
)
|
||||
|
||||
@@ -1916,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"
|
||||
@@ -1932,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")
|
||||
@@ -1942,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.
|
||||
@@ -1983,7 +2028,7 @@ def test_fetch_deleted_msg(acfactory, lp):
|
||||
if ev.name == "DC_EVENT_MSGS_CHANGED":
|
||||
pytest.fail("A deleted message was shown to the user")
|
||||
|
||||
if ev.name == "DC_EVENT_INFO" and 'IDLE entering wait-on-remote state in folder "INBOX".' in ev.data2:
|
||||
if ev.name == "DC_EVENT_INFO" and "INBOX: Idle entering wait-on-remote state" in ev.data2:
|
||||
break # DC is done with reading messages
|
||||
|
||||
|
||||
@@ -2295,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()
|
||||
|
||||
107
python/tests/test_2_increation.py
Normal file
107
python/tests/test_2_increation.py
Normal 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)
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-02-10
|
||||
2024-11-26
|
||||
@@ -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.1
|
||||
RUST_VERSION=1.82.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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"
|
||||
|
||||
990
src/blob.rs
990
src/blob.rs
File diff suppressed because it is too large
Load Diff
@@ -1,810 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::sql;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = ImageReader::open(path)
|
||||
.expect("failed to open image")
|
||||
.with_guessed_format()
|
||||
.expect("failed to guess format")
|
||||
.decode()
|
||||
.expect("failed to decode image");
|
||||
assert_eq!(img.width(), width, "invalid width");
|
||||
assert_eq!(img.height(), height, "invalid height");
|
||||
img
|
||||
})
|
||||
}
|
||||
|
||||
const FILE_BYTES: &[u8] = b"hello";
|
||||
const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
let fname = t.get_blobdir().join(FILE_DEDUPLICATED);
|
||||
let data = fs::read(fname).await.unwrap();
|
||||
assert_eq!(data, FILE_BYTES);
|
||||
assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}"));
|
||||
assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lowercase_ext() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap();
|
||||
assert!(
|
||||
blob.as_name().ends_with(".txt"),
|
||||
"Blob {blob:?} should end with .txt"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_as_file_name() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
assert_eq!(blob.as_file_name(), FILE_DEDUPLICATED);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_as_rel_path() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
assert_eq!(blob.as_rel_path(), Path::new(FILE_DEDUPLICATED));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_suffix() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
assert_eq!(blob.suffix(), Some("txt"));
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap();
|
||||
assert_eq!(blob.suffix(), None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_dup() {
|
||||
let t = TestContext::new().await;
|
||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED);
|
||||
assert!(foo_path.exists());
|
||||
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap();
|
||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
||||
while let Ok(Some(dirent)) = dir.next_entry().await {
|
||||
let fname = dirent.file_name();
|
||||
if fname == foo_path.file_name().unwrap() {
|
||||
assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
|
||||
} else {
|
||||
let name = fname.to_str().unwrap();
|
||||
assert!(name.ends_with(".txt"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_double_ext() {
|
||||
let t = TestContext::new().await;
|
||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap();
|
||||
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz");
|
||||
assert!(foo_path.exists());
|
||||
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap();
|
||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
||||
while let Ok(Some(dirent)) = dir.next_entry().await {
|
||||
let fname = dirent.file_name();
|
||||
if fname == foo_path.file_name().unwrap() {
|
||||
assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
|
||||
} else {
|
||||
let name = fname.to_str().unwrap();
|
||||
println!("{name}");
|
||||
assert_eq!(name.starts_with("foo"), false);
|
||||
assert_eq!(name.ends_with(".tar.gz"), false);
|
||||
assert!(name.ends_with(".gz"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_long_names() {
|
||||
let t = TestContext::new().await;
|
||||
let s = format!("file.{}", "a".repeat(100));
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
|
||||
let blobname = blob.as_name().split('/').last().unwrap();
|
||||
assert!(blobname.len() < 70);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_and_copy() {
|
||||
let t = TestContext::new().await;
|
||||
let src = t.dir.path().join("src");
|
||||
fs::write(&src, b"boo").await.unwrap();
|
||||
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
|
||||
let whoops = t.dir.path().join("whoops");
|
||||
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
|
||||
.await
|
||||
.is_err());
|
||||
let whoops = t.get_blobdir().join("whoops");
|
||||
assert!(!whoops.exists());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_from_path() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let src_ext = t.dir.path().join("external");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
|
||||
let src_int = t.get_blobdir().join("internal");
|
||||
fs::write(&src_int, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_from_name_long() {
|
||||
let t = TestContext::new().await;
|
||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
blob.as_name(),
|
||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_blob_name() {
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitise_name() {
|
||||
let (stem, ext) = BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
|
||||
assert_eq!(ext, ".txt");
|
||||
assert!(!stem.is_empty());
|
||||
|
||||
// the extensions are kept together as between stem and extension a number may be added -
|
||||
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
|
||||
let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
|
||||
assert_eq!(stem, "wot");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
|
||||
assert_eq!(stem, "");
|
||||
assert_eq!(ext, ".foo.bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
|
||||
assert!(stem.contains("foo"));
|
||||
assert!(!stem.contains('?'));
|
||||
assert_eq!(ext, ".bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("no-extension");
|
||||
assert_eq!(stem, "no-extension");
|
||||
assert_eq!(ext, "");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
|
||||
assert_eq!(ext, ".c");
|
||||
assert!(!stem.contains("path"));
|
||||
assert!(!stem.contains("ignored"));
|
||||
assert!(stem.contains("this"));
|
||||
assert!(stem.contains("forbidden"));
|
||||
assert!(!stem.contains('/'));
|
||||
assert!(!stem.contains('\\'));
|
||||
assert!(!stem.contains(':'));
|
||||
assert!(!stem.contains('*'));
|
||||
assert!(!stem.contains('?'));
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name(
|
||||
"file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
|
||||
);
|
||||
assert_eq!(
|
||||
stem,
|
||||
"file.with_lots_of_characters_behind_point_and_double_ending"
|
||||
);
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
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)]
|
||||
async fn test_add_white_bg() {
|
||||
let t = TestContext::new().await;
|
||||
let bytes0 = include_bytes!("../../test-data/image/logo.png").as_slice();
|
||||
let bytes1 = include_bytes!("../../test-data/image/avatar900x900.png").as_slice();
|
||||
for (bytes, color) in [
|
||||
(bytes0, [255u8, 255, 255, 255]),
|
||||
(bytes1, [253u8, 198, 0, 255]),
|
||||
] {
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
fs::write(&avatar_src, bytes).await.unwrap();
|
||||
|
||||
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
|
||||
let img_wh = 128;
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
|
||||
.unwrap();
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = ImageReader::open(blob.to_abs_path())
|
||||
.unwrap()
|
||||
.with_guessed_format()
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap();
|
||||
assert!(img.width() == img_wh);
|
||||
assert!(img.height() == img_wh);
|
||||
assert_eq!(img.get_pixel(0, 0), Rgba(color));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_outside_blobdir() {
|
||||
async fn file_size(path_buf: &Path) -> u64 {
|
||||
fs::metadata(path_buf).await.unwrap().len()
|
||||
}
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
let avatar_path = Path::new(&avatar_blob);
|
||||
assert!(
|
||||
avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"),
|
||||
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
||||
);
|
||||
let scaled_avatar_size = file_size(avatar_path).await;
|
||||
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
|
||||
|
||||
check_image_size(avatar_src, 1000, 1000);
|
||||
check_image_size(
|
||||
&avatar_blob,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
);
|
||||
|
||||
let mut blob = BlobObject::new_from_path(&t, avatar_path).await.unwrap();
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
|
||||
.unwrap();
|
||||
let new_file_size = file_size(&blob.to_abs_path()).await;
|
||||
assert!(new_file_size <= 3000);
|
||||
assert!(new_file_size > 2000);
|
||||
// The new file should be smaller:
|
||||
assert!(new_file_size < scaled_avatar_size);
|
||||
// And the original file should not be touched:
|
||||
assert_eq!(file_size(avatar_path).await, scaled_avatar_size);
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = ImageReader::open(blob.to_abs_path())
|
||||
.unwrap()
|
||||
.with_guessed_format()
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap();
|
||||
assert!(img.width() > 130);
|
||||
assert_eq!(img.width(), img.height());
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_in_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.get_blobdir().join("avatar.png");
|
||||
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
check_image_size(&avatar_src, 900, 900);
|
||||
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert!(
|
||||
avatar_cfg.ends_with("9e7f409ac5c92b942cc4f31cee2770a.png"),
|
||||
"Avatar file name {avatar_cfg} should end with its hash"
|
||||
);
|
||||
|
||||
check_image_size(
|
||||
avatar_cfg,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_copy_without_recode() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
|
||||
assert!(!avatar_blob.exists());
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(avatar_blob.exists());
|
||||
assert_eq!(
|
||||
fs::metadata(&avatar_blob).await.unwrap().len(),
|
||||
avatar_bytes.len() as u64
|
||||
);
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_1() {
|
||||
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_2() {
|
||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||
let bytes = include_bytes!("../../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 2000,
|
||||
original_height: 1800,
|
||||
orientation: 270,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let mut buf = Cursor::new(vec![]);
|
||||
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
|
||||
let bytes = buf.into_inner();
|
||||
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes: &bytes,
|
||||
extension: "jpg",
|
||||
original_width: 1800,
|
||||
original_height: 2000,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_balanced_png() {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
set_draft: true,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Tests that RGBA PNG can be recoded into JPEG
|
||||
/// by dropping alpha channel.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot-rgba.png");
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_huge_jpg() {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot.jpg");
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
||||
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_correct_rotation(img: &DynamicImage) {
|
||||
// The test images are black in the bottom left corner after correctly applying
|
||||
// the EXIF orientation
|
||||
|
||||
let [luma] = img.get_pixel(10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img
|
||||
.get_pixel(img.width() - 10, img.height() - 10)
|
||||
.to_luma()
|
||||
.0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
|
||||
assert_eq!(luma, 0);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SendImageCheckMediaquality<'a> {
|
||||
pub(crate) viewtype: Viewtype,
|
||||
pub(crate) media_quality_config: &'a str,
|
||||
pub(crate) bytes: &'a [u8],
|
||||
pub(crate) extension: &'a str,
|
||||
pub(crate) has_exif: bool,
|
||||
pub(crate) original_width: u32,
|
||||
pub(crate) original_height: u32,
|
||||
pub(crate) orientation: i32,
|
||||
pub(crate) compressed_width: u32,
|
||||
pub(crate) compressed_height: u32,
|
||||
pub(crate) set_draft: bool,
|
||||
}
|
||||
|
||||
impl SendImageCheckMediaquality<'_> {
|
||||
pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
|
||||
let viewtype = self.viewtype;
|
||||
let media_quality_config = self.media_quality_config;
|
||||
let bytes = self.bytes;
|
||||
let extension = self.extension;
|
||||
let has_exif = self.has_exif;
|
||||
let original_width = self.original_width;
|
||||
let original_height = self.original_height;
|
||||
let orientation = self.orientation;
|
||||
let compressed_width = self.compressed_width;
|
||||
let compressed_height = self.compressed_height;
|
||||
let set_draft = self.set_draft;
|
||||
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, Some(media_quality_config))
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension(extension);
|
||||
let file_name = format!("file.{extension}");
|
||||
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
check_image_size(&file, original_width, original_height);
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
||||
if has_exif {
|
||||
let exif = exif.unwrap();
|
||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||
} else {
|
||||
assert!(exif.is_none());
|
||||
}
|
||||
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
if set_draft {
|
||||
chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
||||
msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
}
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = alice
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||
alice_msg.save_file(&alice, &file_saved).await?;
|
||||
check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
if viewtype == Viewtype::File {
|
||||
assert_eq!(file_saved.extension().unwrap(), extension);
|
||||
let bytes1 = fs::read(&file_saved).await?;
|
||||
assert_eq!(&bytes1, bytes);
|
||||
}
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_big_gif_as_image() -> Result<()> {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let (width, height) = (1920u32, 1080u32);
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(
|
||||
Config::MediaQuality,
|
||||
Some(&(MediaQuality::Worse as i32).to_string()),
|
||||
)
|
||||
.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::Image);
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
// DC must detect the image as GIF and send it w/o reencoding.
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
|
||||
assert_eq!(bob_msg.get_width() as u32, width);
|
||||
assert_eq!(bob_msg.get_height() as u32, height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert_eq!(file_size, bytes.len() as u64);
|
||||
check_image_size(file_saved, width, height);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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);
|
||||
msg.set_file_and_deduplicate(alice, &file, None, 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);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_and_deduplicate() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let path = t.get_blobdir().join("anyfile.dat");
|
||||
fs::write(&path, b"bla").await?;
|
||||
let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat");
|
||||
assert_eq!(path.exists(), false);
|
||||
|
||||
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
||||
|
||||
fs::write(&path, b"bla").await?;
|
||||
let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||
assert_eq!(blob2.name, blob.name);
|
||||
|
||||
let path_outside_blobdir = t.dir.path().join("anyfile.dat");
|
||||
fs::write(&path_outside_blobdir, b"bla").await?;
|
||||
let blob3 =
|
||||
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
||||
assert!(path_outside_blobdir.exists());
|
||||
assert_eq!(blob3.name, blob.name);
|
||||
|
||||
fs::write(&path, b"blabla").await?;
|
||||
let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||
assert_ne!(blob4.name, blob.name);
|
||||
|
||||
fs::remove_dir_all(t.get_blobdir()).await?;
|
||||
let blob5 =
|
||||
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
||||
assert_eq!(blob5.name, blob.name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
fs::remove_dir(t.get_blobdir()).await?;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
||||
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f");
|
||||
|
||||
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
||||
let modified1 = blob.to_abs_path().metadata()?.modified()?;
|
||||
|
||||
// Test that the modification time of the file is updated when a new file is created
|
||||
// so that it's not deleted during housekeeping.
|
||||
// We can't use SystemTime::shift() here because file creation uses the actual OS time,
|
||||
// which we can't mock from our code.
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
|
||||
let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
||||
assert_eq!(blob2.name, blob.name);
|
||||
|
||||
let modified2 = blob.to_abs_path().metadata()?.modified()?;
|
||||
assert_ne!(modified1, modified2);
|
||||
sql::housekeeping(&t).await?;
|
||||
assert!(blob2.to_abs_path().exists());
|
||||
|
||||
// If we do shift the time by more than 1h, the blob file will be deleted during housekeeping:
|
||||
SystemTime::shift(Duration::from_secs(65 * 60));
|
||||
sql::housekeeping(&t).await?;
|
||||
assert_eq!(blob2.to_abs_path().exists(), false);
|
||||
|
||||
let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
||||
assert_ne!(blob3.name, blob.name);
|
||||
|
||||
{
|
||||
// If something goes wrong and the blob file is overwritten,
|
||||
// the correct content should be restored:
|
||||
fs::write(blob3.to_abs_path(), b"bloblo").await?;
|
||||
|
||||
let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
||||
let blob4_content = fs::read(blob4.to_abs_path()).await?;
|
||||
assert_eq!(blob4_content, b"blabla");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
3880
src/chat.rs
3880
src/chat.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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)]
|
||||
|
||||
@@ -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,10 +143,7 @@ pub enum Config {
|
||||
/// Send BCC copy to self.
|
||||
///
|
||||
/// Should be enabled for multidevice setups.
|
||||
/// Default is 0 for chatmail accounts, 1 otherwise.
|
||||
///
|
||||
/// This is automatically enabled when importing/exporting a backup,
|
||||
/// setting up a second device, or receiving a sync message.
|
||||
#[strum(props(default = "1"))]
|
||||
BccSelf,
|
||||
|
||||
/// True if encryption is preferred according to Autocrypt standard.
|
||||
@@ -205,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
|
||||
@@ -387,11 +384,6 @@ pub enum Config {
|
||||
|
||||
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set
|
||||
/// and `Bot` unset.
|
||||
///
|
||||
/// On real devices, this is usually always enabled and `BccSelf` is the only setting
|
||||
/// that controls whether sync messages are sent.
|
||||
///
|
||||
/// In tests, this is usually disabled.
|
||||
#[strum(props(default = "1"))]
|
||||
SyncMsgs,
|
||||
|
||||
@@ -449,19 +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,
|
||||
|
||||
/// Device token encrypted with OpenPGP.
|
||||
///
|
||||
/// We store encrypted token next to `device_token`
|
||||
/// to avoid encrypting it differently and
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -534,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()))
|
||||
@@ -693,7 +664,7 @@ impl Context {
|
||||
let value = match key {
|
||||
Config::Selfavatar if value.is_empty() => None,
|
||||
Config::Selfavatar => {
|
||||
config_value = BlobObject::store_from_base64(self, value)?;
|
||||
config_value = BlobObject::store_from_base64(self, value, "avatar").await?;
|
||||
Some(config_value.as_str())
|
||||
}
|
||||
_ => Some(value),
|
||||
@@ -771,8 +742,7 @@ impl Context {
|
||||
.await?;
|
||||
match value {
|
||||
Some(path) => {
|
||||
let path = get_abs_path(self, Path::new(path));
|
||||
let mut blob = BlobObject::create_and_deduplicate(self, &path, &path)?;
|
||||
let mut blob = BlobObject::new_from_path(self, path.as_ref()).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql
|
||||
.set_raw_config(key.as_ref(), Some(blob.as_name()))
|
||||
@@ -1129,30 +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(())
|
||||
}
|
||||
|
||||
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync() -> Result<()> {
|
||||
let alice0 = TestContext::new_alice().await;
|
||||
@@ -1227,7 +1173,7 @@ mod tests {
|
||||
let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap();
|
||||
assert_eq!(
|
||||
self_chat_avatar_path,
|
||||
alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE)
|
||||
alice0.get_blobdir().join("icon-saved-messages.png")
|
||||
);
|
||||
assert!(alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
|
||||
1366
src/contact.rs
1366
src/contact.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
711
src/context.rs
711
src/context.rs
@@ -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);
|
||||
@@ -1480,4 +1461,652 @@ pub fn get_version_str() -> &'static str {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod context_tests;
|
||||
mod tests {
|
||||
use anyhow::Context as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
tokio::fs::write(&dbfile, b"123").await?;
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
|
||||
|
||||
// Broken database is indistinguishable from encrypted one.
|
||||
assert_eq!(res.is_open().await, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let fresh = t.get_fresh_msgs().await.unwrap();
|
||||
assert!(fresh.is_empty())
|
||||
}
|
||||
|
||||
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
||||
let contact = Contact::get_by_id(t, *members.first().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
"From: {}\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <{}>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
contact.get_addr(),
|
||||
create_outgoing_rfc724_mid()
|
||||
);
|
||||
println!("{msg}");
|
||||
receive_imf(t, msg.as_bytes(), false).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_chats() {
|
||||
// receive various mails in 3 chats
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
let claire = t.create_chat_with_contact("", "claire@g.it").await;
|
||||
let dave = t.create_chat_with_contact("", "dave@g.it").await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
||||
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3);
|
||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
||||
|
||||
// mute one of the chats
|
||||
set_muted(&t, claire.id, MuteDuration::Forever)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
|
||||
|
||||
// receive more messages
|
||||
receive_msg(&t, &bob).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
||||
|
||||
// unmute claire again
|
||||
set_muted(&t, claire.id, MuteDuration::NotMuted)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_until() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
|
||||
// chat is unmuted by default, here and in the following assert(),
|
||||
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
||||
// have the same view to the database.
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with mute_until in the future
|
||||
set_muted(
|
||||
&t,
|
||||
bob.id,
|
||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with mute_until in the past,
|
||||
// we need to modify the database directly
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
(time() - 3600, bob.id),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with "forever" mute_until
|
||||
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||
// that results in "muted forever" by definition.
|
||||
t.sql
|
||||
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_muted_context() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
t.set_config(Config::IsMuted, Some("1")).await?;
|
||||
let chat = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &chat).await;
|
||||
|
||||
// muted contexts should still show dimmed badge counters eg. in the sidebars,
|
||||
// (same as muted chats show dimmed badge counters in the chatlist)
|
||||
// therefore the fresh messages count should not be affected.
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_blogdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
tokio::fs::write(&blobdir, b"123").await.unwrap();
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sqlite_parent_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_empty_blobdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir(
|
||||
dbfile,
|
||||
blobdir,
|
||||
1,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_blobdir_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir(
|
||||
dbfile,
|
||||
blobdir,
|
||||
1,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn no_crashes_on_context_deref() {
|
||||
let t = TestContext::new().await;
|
||||
std::mem::drop(t);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_info() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let info = t.get_info().await.unwrap();
|
||||
assert!(info.contains_key("database_dir"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info_no_context() {
|
||||
let info = get_info();
|
||||
assert!(info.contains_key("deltachat_core_version"));
|
||||
assert!(!info.contains_key("database_dir"));
|
||||
assert_eq!(info.get("level").unwrap(), "awesome");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_info_completeness() {
|
||||
// For easier debugging,
|
||||
// get_info() shall return all important information configurable by the Config-values.
|
||||
//
|
||||
// There are exceptions for Config-values considered to be unimportant,
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
"mail_user",
|
||||
"mail_pw",
|
||||
"mail_port",
|
||||
"mail_security",
|
||||
"notify_about_wrong_pw",
|
||||
"self_reporting_id",
|
||||
"selfstatus",
|
||||
"send_server",
|
||||
"send_user",
|
||||
"send_pw",
|
||||
"send_port",
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"smtp_certificate_checks",
|
||||
"proxy_url", // May contain passwords, don't leak it to the logs.
|
||||
"socks5_enabled", // SOCKS5 options are deprecated.
|
||||
"socks5_host",
|
||||
"socks5_port",
|
||||
"socks5_user",
|
||||
"socks5_password",
|
||||
"key_id",
|
||||
"webxdc_integration",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
for key in Config::iter() {
|
||||
let key: String = key.to_string();
|
||||
if !skip_from_get_info.contains(&&*key)
|
||||
&& !key.starts_with("configured")
|
||||
&& !key.starts_with("sys.")
|
||||
{
|
||||
assert!(
|
||||
info.contains_key(&*key),
|
||||
"'{key}' missing in get_info() output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Global search finds nothing.
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds nothing.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Add messages to chat with Bob.
|
||||
let mut msg1 = Message::new_text("foobar".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg1).await?;
|
||||
|
||||
let mut msg2 = Message::new_text("barbaz".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||
|
||||
alice.send_text(chat.id, "Δ-Chat").await;
|
||||
|
||||
// Global search with a part of text finds the message.
|
||||
let res = alice.search_msgs(None, "ob").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Global search for "bar" matches both "foobar" and "barbaz".
|
||||
let res = alice.search_msgs(None, "bar").await?;
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
// Message added later is returned first.
|
||||
assert_eq!(res.first(), Some(&msg2.id));
|
||||
assert_eq!(res.get(1), Some(&msg1.id));
|
||||
|
||||
// Search is case-insensitive.
|
||||
for chat_id in [None, Some(chat.id)] {
|
||||
let res = alice.search_msgs(chat_id, "δ-chat").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
// Global search with longer text does not find any message.
|
||||
let res = alice.search_msgs(None, "foobarbaz").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search for random string finds nothing.
|
||||
let res = alice.search_msgs(None, "abc").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds the message.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Search in Saved Messages does not find the message.
|
||||
let res = alice.search_msgs(Some(self_talk), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_unaccepted_requests() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: BobBar <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
||||
\n\
|
||||
hello bob, foobar test!\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Single);
|
||||
assert!(chat.is_contact_request());
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
chat_id.block(&t).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
||||
|
||||
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
||||
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_limit_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Add 999 messages
|
||||
let mut msg = Message::new_text("foobar".to_string());
|
||||
for _ in 0..999 {
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
}
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 999);
|
||||
|
||||
// Add one more message, no limit yet
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// Add one more message, that one is truncated then
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// In-chat should not be not limited
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1001);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile.clone())
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
drop(context);
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(2)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
||||
assert_eq!(context.open("false".to_string()).await?, false);
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_context_change_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
|
||||
context
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("Failed to change passphrase")?;
|
||||
|
||||
assert_eq!(
|
||||
context.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ongoing() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
// No ongoing process allocated.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
let receiver = context.alloc_ongoing().await?;
|
||||
|
||||
// Cannot allocate another ongoing process while the first one is running.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
// Stop signal is not sent yet.
|
||||
assert!(receiver.try_recv().is_err());
|
||||
|
||||
assert!(!context.shall_stop_ongoing().await);
|
||||
|
||||
// Send the stop signal.
|
||||
context.stop_ongoing().await;
|
||||
|
||||
// Receive stop signal.
|
||||
receiver.recv().await?;
|
||||
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Ongoing process is still running even though stop signal was received,
|
||||
// so another one cannot be allocated.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
// No ongoing process allocated, should have been stopped already.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Another ongoing process can be allocated now.
|
||||
let _receiver = context.alloc_ongoing().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_next_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
let bob_next_msg_ids = bob.get_next_msgs().await?;
|
||||
assert_eq!(bob_next_msg_ids.len(), 1);
|
||||
assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id));
|
||||
|
||||
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
|
||||
.await?;
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
// Next messages include self-sent messages.
|
||||
let alice_next_msg_ids = alice.get_next_msgs().await?;
|
||||
assert_eq!(alice_next_msg_ids.len(), 1);
|
||||
assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id));
|
||||
|
||||
alice
|
||||
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
|
||||
.await?;
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_draft_self_report() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = alice.draft_self_report().await?;
|
||||
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
|
||||
assert!(draft.text.starts_with("core_version"));
|
||||
|
||||
// Test that sending into the protected chat works:
|
||||
let _sent = alice.send_msg(chat_id, &mut draft).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Change the config circumventing the cache
|
||||
// This simulates what the notification plugin on iOS might do
|
||||
// because it runs in a different process
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice's Delta Chat doesn't know about it yet:
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Starting IO will fail of course because no server settings are configured,
|
||||
// but it should invalidate the caches:
|
||||
alice.start_io().await;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,649 +0,0 @@
|
||||
use anyhow::Context as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
tokio::fs::write(&dbfile, b"123").await?;
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
|
||||
|
||||
// Broken database is indistinguishable from encrypted one.
|
||||
assert_eq!(res.is_open().await, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let fresh = t.get_fresh_msgs().await.unwrap();
|
||||
assert!(fresh.is_empty())
|
||||
}
|
||||
|
||||
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
||||
let contact = Contact::get_by_id(t, *members.first().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
"From: {}\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <{}>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
contact.get_addr(),
|
||||
create_outgoing_rfc724_mid()
|
||||
);
|
||||
println!("{msg}");
|
||||
receive_imf(t, msg.as_bytes(), false).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_chats() {
|
||||
// receive various mails in 3 chats
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
let claire = t.create_chat_with_contact("", "claire@g.it").await;
|
||||
let dave = t.create_chat_with_contact("", "dave@g.it").await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
||||
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3);
|
||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
||||
|
||||
// mute one of the chats
|
||||
set_muted(&t, claire.id, MuteDuration::Forever)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
|
||||
|
||||
// receive more messages
|
||||
receive_msg(&t, &bob).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
||||
|
||||
// unmute claire again
|
||||
set_muted(&t, claire.id, MuteDuration::NotMuted)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_until() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
|
||||
// chat is unmuted by default, here and in the following assert(),
|
||||
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
||||
// have the same view to the database.
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with mute_until in the future
|
||||
set_muted(
|
||||
&t,
|
||||
bob.id,
|
||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with mute_until in the past,
|
||||
// we need to modify the database directly
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
(time() - 3600, bob.id),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with "forever" mute_until
|
||||
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||
// that results in "muted forever" by definition.
|
||||
t.sql
|
||||
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_muted_context() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
t.set_config(Config::IsMuted, Some("1")).await?;
|
||||
let chat = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &chat).await;
|
||||
|
||||
// muted contexts should still show dimmed badge counters eg. in the sidebars,
|
||||
// (same as muted chats show dimmed badge counters in the chatlist)
|
||||
// therefore the fresh messages count should not be affected.
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_blogdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
tokio::fs::write(&blobdir, b"123").await.unwrap();
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sqlite_parent_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_empty_blobdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir(
|
||||
dbfile,
|
||||
blobdir,
|
||||
1,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_blobdir_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir(
|
||||
dbfile,
|
||||
blobdir,
|
||||
1,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn no_crashes_on_context_deref() {
|
||||
let t = TestContext::new().await;
|
||||
std::mem::drop(t);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_info() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let info = t.get_info().await.unwrap();
|
||||
assert!(info.contains_key("database_dir"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info_no_context() {
|
||||
let info = get_info();
|
||||
assert!(info.contains_key("deltachat_core_version"));
|
||||
assert!(!info.contains_key("database_dir"));
|
||||
assert_eq!(info.get("level").unwrap(), "awesome");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_info_completeness() {
|
||||
// For easier debugging,
|
||||
// get_info() shall return all important information configurable by the Config-values.
|
||||
//
|
||||
// There are exceptions for Config-values considered to be unimportant,
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
"mail_user",
|
||||
"mail_pw",
|
||||
"mail_port",
|
||||
"mail_security",
|
||||
"notify_about_wrong_pw",
|
||||
"self_reporting_id",
|
||||
"selfstatus",
|
||||
"send_server",
|
||||
"send_user",
|
||||
"send_pw",
|
||||
"send_port",
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"smtp_certificate_checks",
|
||||
"proxy_url", // May contain passwords, don't leak it to the logs.
|
||||
"socks5_enabled", // SOCKS5 options are deprecated.
|
||||
"socks5_host",
|
||||
"socks5_port",
|
||||
"socks5_user",
|
||||
"socks5_password",
|
||||
"key_id",
|
||||
"webxdc_integration",
|
||||
"device_token",
|
||||
"encrypted_device_token",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
for key in Config::iter() {
|
||||
let key: String = key.to_string();
|
||||
if !skip_from_get_info.contains(&&*key)
|
||||
&& !key.starts_with("configured")
|
||||
&& !key.starts_with("sys.")
|
||||
{
|
||||
assert!(
|
||||
info.contains_key(&*key),
|
||||
"'{key}' missing in get_info() output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Global search finds nothing.
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds nothing.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Add messages to chat with Bob.
|
||||
let mut msg1 = Message::new_text("foobar".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg1).await?;
|
||||
|
||||
let mut msg2 = Message::new_text("barbaz".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||
|
||||
alice.send_text(chat.id, "Δ-Chat").await;
|
||||
|
||||
// Global search with a part of text finds the message.
|
||||
let res = alice.search_msgs(None, "ob").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Global search for "bar" matches both "foobar" and "barbaz".
|
||||
let res = alice.search_msgs(None, "bar").await?;
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
// Message added later is returned first.
|
||||
assert_eq!(res.first(), Some(&msg2.id));
|
||||
assert_eq!(res.get(1), Some(&msg1.id));
|
||||
|
||||
// Search is case-insensitive.
|
||||
for chat_id in [None, Some(chat.id)] {
|
||||
let res = alice.search_msgs(chat_id, "δ-chat").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
// Global search with longer text does not find any message.
|
||||
let res = alice.search_msgs(None, "foobarbaz").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search for random string finds nothing.
|
||||
let res = alice.search_msgs(None, "abc").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds the message.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Search in Saved Messages does not find the message.
|
||||
let res = alice.search_msgs(Some(self_talk), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_unaccepted_requests() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: BobBar <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
||||
\n\
|
||||
hello bob, foobar test!\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Single);
|
||||
assert!(chat.is_contact_request());
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
chat_id.block(&t).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
||||
|
||||
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
||||
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_limit_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Add 999 messages
|
||||
let mut msg = Message::new_text("foobar".to_string());
|
||||
for _ in 0..999 {
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
}
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 999);
|
||||
|
||||
// Add one more message, no limit yet
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// Add one more message, that one is truncated then
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// In-chat should not be not limited
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1001);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile.clone())
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
drop(context);
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(2)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
||||
assert_eq!(context.open("false".to_string()).await?, false);
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_context_change_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
|
||||
context
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("Failed to change passphrase")?;
|
||||
|
||||
assert_eq!(
|
||||
context.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ongoing() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
// No ongoing process allocated.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
let receiver = context.alloc_ongoing().await?;
|
||||
|
||||
// Cannot allocate another ongoing process while the first one is running.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
// Stop signal is not sent yet.
|
||||
assert!(receiver.try_recv().is_err());
|
||||
|
||||
assert!(!context.shall_stop_ongoing().await);
|
||||
|
||||
// Send the stop signal.
|
||||
context.stop_ongoing().await;
|
||||
|
||||
// Receive stop signal.
|
||||
receiver.recv().await?;
|
||||
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Ongoing process is still running even though stop signal was received,
|
||||
// so another one cannot be allocated.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
// No ongoing process allocated, should have been stopped already.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Another ongoing process can be allocated now.
|
||||
let _receiver = context.alloc_ongoing().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_next_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
let bob_next_msg_ids = bob.get_next_msgs().await?;
|
||||
assert_eq!(bob_next_msg_ids.len(), 1);
|
||||
assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id));
|
||||
|
||||
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
|
||||
.await?;
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
// Next messages include self-sent messages.
|
||||
let alice_next_msg_ids = alice.get_next_msgs().await?;
|
||||
assert_eq!(alice_next_msg_ids.len(), 1);
|
||||
assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id));
|
||||
|
||||
alice
|
||||
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
|
||||
.await?;
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_draft_self_report() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = alice.draft_self_report().await?;
|
||||
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
|
||||
assert!(draft.text.starts_with("core_version"));
|
||||
|
||||
// Test that sending into the protected chat works:
|
||||
let _sent = alice.send_msg(chat_id, &mut draft).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Change the config circumventing the cache
|
||||
// This simulates what the notification plugin on iOS might do
|
||||
// because it runs in a different process
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice's Delta Chat doesn't know about it yet:
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Starting IO will fail of course because no server settings are configured,
|
||||
// but it should invalidate the caches:
|
||||
alice.start_io().await;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -100,9 +100,7 @@ pub async fn maybe_set_logging_xdc(
|
||||
context,
|
||||
msg.get_viewtype(),
|
||||
chat_id,
|
||||
msg.param
|
||||
.get_path(Param::Filename, context)
|
||||
.unwrap_or_default(),
|
||||
msg.param.get_path(Param::File, context).unwrap_or_default(),
|
||||
msg.get_id(),
|
||||
)
|
||||
.await?;
|
||||
@@ -115,11 +113,11 @@ pub async fn maybe_set_logging_xdc_inner(
|
||||
context: &Context,
|
||||
viewtype: Viewtype,
|
||||
chat_id: ChatId,
|
||||
filename: Option<PathBuf>,
|
||||
file: Option<PathBuf>,
|
||||
msg_id: MsgId,
|
||||
) -> anyhow::Result<()> {
|
||||
if viewtype == Viewtype::Webxdc {
|
||||
if let Some(file) = filename {
|
||||
if let Some(file) = file {
|
||||
if let Some(file_name) = file.file_name().and_then(|name| name.to_str()) {
|
||||
if file_name.starts_with("debug_logging")
|
||||
&& file_name.ends_with(".xdc")
|
||||
|
||||
@@ -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);
|
||||
@@ -440,7 +436,7 @@ mod tests {
|
||||
let file = alice.get_blobdir().join("minimal.xdc");
|
||||
tokio::fs::write(&file, include_bytes!("../test-data/webxdc/minimal.xdc")).await?;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance.set_file_and_deduplicate(&alice, &file, None, None)?;
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
|
||||
|
||||
alice
|
||||
|
||||
128
src/e2ee.rs
128
src/e2ee.rs
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
786
src/ephemeral.rs
786
src/ephemeral.rs
@@ -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 {
|
||||
@@ -713,4 +693,736 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ephemeral_tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::download::DownloadState;
|
||||
use crate::location;
|
||||
use crate::message::markseen_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
||||
use crate::{
|
||||
chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus},
|
||||
tools::IsNoneOrEmpty,
|
||||
};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_ephemeral_messages() {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
|
||||
"You disabled message deletion timer."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 1 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 30 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 30 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 90 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1.5 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 30 * 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 30 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 60 * 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 hour."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 5400 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1.5 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 2 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 day."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 2 days."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 7 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 week."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 4 * 7 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 4 weeks."
|
||||
);
|
||||
}
|
||||
|
||||
/// Test enabling and disabling ephemeral timer remotely.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that enabling ephemeral timer in unpromoted group does not send a message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
|
||||
// Group is unpromoted, the timer can be changed without sending a message.
|
||||
assert!(chat_id.is_unpromoted(&alice).await?);
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_none());
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Promote the group.
|
||||
send_text_msg(&alice, chat_id, "hi!".to_string()).await?;
|
||||
assert!(chat_id.is_promoted(&alice).await?);
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_lost() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice enables the timer.
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
// The message enabling the timer is lost.
|
||||
let _sent = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled,
|
||||
);
|
||||
|
||||
// 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;
|
||||
|
||||
// Bob receives text message and enables the timer, even though explicit timer update was
|
||||
// lost previously.
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
||||
/// timer does not result in disabling the timer on the Bob's side.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// 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;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
// Bob sets ephemeral timer and sends a message about timer change
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent_timer_change = bob.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob receives message from Alice.
|
||||
// Alice message has no timer. However, Bob should not disable timer,
|
||||
// because Alice replies to old message.
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Alice receives message from Bob
|
||||
alice.recv_msg(&sent_timer_change).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob disables the chat timer.
|
||||
// Note that the last message in the Bob's chat is from Alice and has no timer,
|
||||
// but the chat timer is enabled.
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let self_chat = t.get_self_chat().await;
|
||||
|
||||
assert_eq!(next_expiration_timestamp(&t).await, None);
|
||||
|
||||
t.send_text(self_chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(self_chat.id).await;
|
||||
msg.id.trash(&t, false).await?;
|
||||
check_msg_is_deleted(&t, &self_chat, msg.id).await;
|
||||
|
||||
self_chat
|
||||
.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a saved message which will be deleted after 3600s
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
|
||||
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
|
||||
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
|
||||
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(
|
||||
&t,
|
||||
msg.sender_msg_id,
|
||||
&bob_chat,
|
||||
now + 1799,
|
||||
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
|
||||
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
|
||||
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
|
||||
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
|
||||
bob_chat
|
||||
.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_will_be_deleted(
|
||||
t: &TestContext,
|
||||
msg_id: MsgId,
|
||||
chat: &Chat,
|
||||
not_deleted_at: i64,
|
||||
deleted_at: i64,
|
||||
) -> Result<()> {
|
||||
let next_expiration = next_expiration_timestamp(t).await.unwrap();
|
||||
|
||||
assert!(next_expiration > not_deleted_at);
|
||||
delete_expired_messages(t, not_deleted_at).await?;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert!(!loaded.text.is_empty());
|
||||
assert_eq!(loaded.chat_id, chat.id);
|
||||
|
||||
assert!(next_expiration < deleted_at);
|
||||
delete_expired_messages(t, deleted_at).await?;
|
||||
t.evtracker
|
||||
.get_matching(|evt| {
|
||||
if let EventType::MsgDeleted {
|
||||
msg_id: event_msg_id,
|
||||
..
|
||||
} = evt
|
||||
{
|
||||
*event_msg_id == msg_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let loaded = Message::load_from_db_optional(t, msg_id).await?;
|
||||
assert!(loaded.is_none());
|
||||
|
||||
// Check that the msg was deleted locally.
|
||||
check_msg_is_deleted(t, chat, msg_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap();
|
||||
// Check that the chat is empty except for possibly info messages:
|
||||
for item in &chat_items {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
|
||||
assert!(msg.is_info())
|
||||
}
|
||||
}
|
||||
|
||||
// Check that if there is a message left, the text and metadata are gone
|
||||
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
||||
assert_eq!(msg.from_id, ContactId::UNDEFINED);
|
||||
assert_eq!(msg.to_id, ContactId::UNDEFINED);
|
||||
assert_eq!(msg.text, "");
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
(1010, now - 23 * HOUR, 0),
|
||||
(1020, now - 21 * HOUR, 0),
|
||||
(1030, now - 19 * HOUR, 0),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
(3000, now + HOUR, 0),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
(&message_id, id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
||||
.await?;
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 3000).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Regression test for a bug in the timer rollback protection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Message with Message-ID <first@example.com> and no timer is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
// Message with Message-ID <second@example.com> is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
|
||||
Ephemeral-Timer: 60\n\
|
||||
\n\
|
||||
second message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
// Message is deleted when its timer expires.
|
||||
msg.id.trash(&alice, false).await?;
|
||||
|
||||
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
// database anymore, so the timer should be applied unconditionally without rollback
|
||||
// protection.
|
||||
//
|
||||
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
||||
// compared received timer value to the timer value of the <first@example.com>. Because
|
||||
// their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
|
||||
// changed explicitly and the change should be ignored.
|
||||
//
|
||||
// The message also contains a quote of the first message to test that only References:
|
||||
// header and not In-Reply-To: is consulted by the rollback protection.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||
References: <first@example.com> <second@example.com>\n\
|
||||
In-Reply-To: <first@example.com>\n\
|
||||
\n\
|
||||
> hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
msg.chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
|
||||
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
|
||||
// successful reconnection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
let now = time();
|
||||
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1)
|
||||
.await?;
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that POI location is deleted when ephemeral message expires.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_poi_location() -> 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 sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
let mut poi_msg = Message::new_text("Here".to_string());
|
||||
poi_msg.set_location(10.0, 20.0);
|
||||
|
||||
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;
|
||||
let bob_received_message = bob.recv_msg(&alice_sent_message).await;
|
||||
markseen_msgs(bob, vec![bob_received_message.id]).await?;
|
||||
|
||||
for account in [alice, bob] {
|
||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 1);
|
||||
}
|
||||
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
for account in [alice, bob] {
|
||||
delete_expired_messages(account, time()).await?;
|
||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
let chat_id = ChatId::new(12345);
|
||||
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,781 +0,0 @@
|
||||
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;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
||||
use crate::{
|
||||
chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus},
|
||||
tools::IsNoneOrEmpty,
|
||||
};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_ephemeral_messages() {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
|
||||
"You disabled message deletion timer."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 30 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 90 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1.5 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 30 * 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 30 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 60 * 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 hour."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 5400 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1.5 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 2 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 day."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 2 days."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 7 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 week."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 4 * 7 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 4 weeks."
|
||||
);
|
||||
}
|
||||
|
||||
/// Test enabling and disabling ephemeral timer remotely.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that enabling ephemeral timer in unpromoted group does not send a message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
|
||||
// Group is unpromoted, the timer can be changed without sending a message.
|
||||
assert!(chat_id.is_unpromoted(&alice).await?);
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_none());
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Promote the group.
|
||||
send_text_msg(&alice, chat_id, "hi!".to_string()).await?;
|
||||
assert!(chat_id.is_promoted(&alice).await?);
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_lost() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice enables the timer.
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
// The message enabling the timer is lost.
|
||||
let _sent = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled,
|
||||
);
|
||||
|
||||
// Alice sends a text message.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives text message and enables the timer, even though explicit timer update was
|
||||
// lost previously.
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
||||
/// timer does not result in disabling the timer on the Bob's side.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice sends message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
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::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
// Bob sets ephemeral timer and sends a message about timer change
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent_timer_change = bob.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob receives message from Alice.
|
||||
// Alice message has no timer. However, Bob should not disable timer,
|
||||
// because Alice replies to old message.
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Alice receives message from Bob
|
||||
alice.recv_msg(&sent_timer_change).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob disables the chat timer.
|
||||
// Note that the last message in the Bob's chat is from Alice and has no timer,
|
||||
// but the chat timer is enabled.
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let self_chat = t.get_self_chat().await;
|
||||
|
||||
assert_eq!(next_expiration_timestamp(&t).await, None);
|
||||
|
||||
t.send_text(self_chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(self_chat.id).await;
|
||||
msg.id.trash(&t, false).await?;
|
||||
check_msg_is_deleted(&t, &self_chat, msg.id).await;
|
||||
|
||||
self_chat
|
||||
.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a saved message which will be deleted after 3600s
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
|
||||
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
|
||||
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
|
||||
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(
|
||||
&t,
|
||||
msg.sender_msg_id,
|
||||
&bob_chat,
|
||||
now + 1799,
|
||||
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
|
||||
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
|
||||
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
|
||||
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
|
||||
bob_chat
|
||||
.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_will_be_deleted(
|
||||
t: &TestContext,
|
||||
msg_id: MsgId,
|
||||
chat: &Chat,
|
||||
not_deleted_at: i64,
|
||||
deleted_at: i64,
|
||||
) -> Result<()> {
|
||||
let next_expiration = next_expiration_timestamp(t).await.unwrap();
|
||||
|
||||
assert!(next_expiration > not_deleted_at);
|
||||
delete_expired_messages(t, not_deleted_at).await?;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert!(!loaded.text.is_empty());
|
||||
assert_eq!(loaded.chat_id, chat.id);
|
||||
|
||||
assert!(next_expiration < deleted_at);
|
||||
delete_expired_messages(t, deleted_at).await?;
|
||||
t.evtracker
|
||||
.get_matching(|evt| {
|
||||
if let EventType::MsgDeleted {
|
||||
msg_id: event_msg_id,
|
||||
..
|
||||
} = evt
|
||||
{
|
||||
*event_msg_id == msg_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let loaded = Message::load_from_db_optional(t, msg_id).await?;
|
||||
assert!(loaded.is_none());
|
||||
|
||||
// Check that the msg was deleted locally.
|
||||
check_msg_is_deleted(t, chat, msg_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap();
|
||||
// Check that the chat is empty except for possibly info messages:
|
||||
for item in &chat_items {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
|
||||
assert!(msg.is_info())
|
||||
}
|
||||
}
|
||||
|
||||
// Check that if there is a message left, the text and metadata are gone
|
||||
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
||||
assert_eq!(msg.from_id, ContactId::UNDEFINED);
|
||||
assert_eq!(msg.to_id, ContactId::UNDEFINED);
|
||||
assert_eq!(msg.text, "");
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
(1010, now - 23 * HOUR, 0),
|
||||
(1020, now - 21 * HOUR, 0),
|
||||
(1030, now - 19 * HOUR, 0),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
(3000, now + HOUR, 0),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
(&message_id, id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
||||
.await?;
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 3000).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Regression test for a bug in the timer rollback protection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Message with Message-ID <first@example.com> and no timer is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
// Message with Message-ID <second@example.com> is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
|
||||
Ephemeral-Timer: 60\n\
|
||||
\n\
|
||||
second message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
// Message is deleted when its timer expires.
|
||||
msg.id.trash(&alice, false).await?;
|
||||
|
||||
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
// database anymore, so the timer should be applied unconditionally without rollback
|
||||
// protection.
|
||||
//
|
||||
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
||||
// compared received timer value to the timer value of the <first@example.com>. Because
|
||||
// their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
|
||||
// changed explicitly and the change should be ignored.
|
||||
//
|
||||
// The message also contains a quote of the first message to test that only References:
|
||||
// header and not In-Reply-To: is consulted by the rollback protection.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||
References: <first@example.com> <second@example.com>\n\
|
||||
In-Reply-To: <first@example.com>\n\
|
||||
\n\
|
||||
> hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
msg.chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
|
||||
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
|
||||
// successful reconnection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
let now = time();
|
||||
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?;
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that POI location is deleted when ephemeral message expires.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_poi_location() -> 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 sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
let mut poi_msg = Message::new_text("Here".to_string());
|
||||
poi_msg.set_location(10.0, 20.0);
|
||||
|
||||
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;
|
||||
let bob_received_message = bob.recv_msg(&alice_sent_message).await;
|
||||
markseen_msgs(bob, vec![bob_received_message.id]).await?;
|
||||
|
||||
for account in [alice, bob] {
|
||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 1);
|
||||
}
|
||||
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
for account in [alice, bob] {
|
||||
delete_expired_messages(account, time()).await?;
|
||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
let chat_id = ChatId::new(12345);
|
||||
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -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.
|
||||
@@ -97,9 +97,6 @@ pub enum EventType {
|
||||
|
||||
/// Reactions for the message changed.
|
||||
IncomingReaction {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the contact whose reaction set is changed.
|
||||
contact_id: ContactId,
|
||||
|
||||
@@ -112,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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
115
src/html.rs
115
src/html.rs
@@ -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);
|
||||
@@ -291,7 +263,7 @@ pub fn new_html_mimepart(html: String) -> PartBuilder {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat;
|
||||
use crate::chat::{forward_msgs, save_msgs};
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
@@ -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>
|
||||
@@ -371,6 +343,7 @@ and will be wrapped as usual.<br/>
|
||||
mime-modified should not be set set as there is no html and no special stuff;<br/>
|
||||
although not being a delta-message.<br/>
|
||||
test some special html-characters as < > and & but also " and ' :)<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
@@ -380,7 +353,7 @@ test some special html-characters as < > and & but also " 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`;
|
||||
@@ -398,12 +371,13 @@ test some special html-characters as < > and & but also " 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>
|
||||
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
|
||||
</html>
|
||||
|
||||
"##
|
||||
);
|
||||
}
|
||||
@@ -412,7 +386,7 @@ test some special html-characters as < > and & but also " 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>
|
||||
@@ -420,6 +394,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
this is <b>html</b>
|
||||
</p>
|
||||
</html>
|
||||
|
||||
"##
|
||||
);
|
||||
}
|
||||
@@ -436,7 +411,7 @@ test some special html-characters as < > and & but also " 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"));
|
||||
@@ -496,38 +471,6 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_save_msg() -> Result<()> {
|
||||
// Alice receives a non-delta html-message
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
receive_imf(&alice, raw, false).await?;
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
|
||||
// Alice saves the message
|
||||
let self_chat = alice.get_self_chat().await;
|
||||
save_msgs(&alice, &[msg.id]).await?;
|
||||
let saved_msg = alice.get_last_msg_in(self_chat.get_id()).await;
|
||||
assert_ne!(saved_msg.id, msg.id);
|
||||
assert_eq!(
|
||||
saved_msg.get_original_msg_id(&alice).await?.unwrap(),
|
||||
msg.id
|
||||
);
|
||||
assert!(!saved_msg.is_forwarded()); // UI should not flag "saved messages" as "forwarded"
|
||||
assert_ne!(saved_msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(saved_msg.get_from_id(), msg.get_from_id());
|
||||
assert_eq!(saved_msg.is_dc_message, MessengerMessage::No);
|
||||
assert!(saved_msg.get_text().contains("this is plain"));
|
||||
assert!(saved_msg.has_html());
|
||||
let html = saved_msg.get_id().get_html(&alice).await?.unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_forwarding_encrypted() {
|
||||
// Alice receives a non-delta html-message
|
||||
|
||||
623
src/imap.rs
623
src/imap.rs
@@ -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
|
||||
@@ -1331,7 +1300,7 @@ impl Session {
|
||||
/// Returns the last UID fetched successfully and the info about each downloaded message.
|
||||
/// If the message is incorrect or there is a failure to write a message to the database,
|
||||
/// it is skipped and the error is logged.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn fetch_many_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -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,74 +1560,17 @@ impl Session {
|
||||
};
|
||||
|
||||
if self.can_metadata() && self.can_push() {
|
||||
let old_encrypted_device_token =
|
||||
context.get_config(Config::EncryptedDeviceToken).await?;
|
||||
let folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
// Whether we need to update encrypted device token.
|
||||
let device_token_changed = old_encrypted_device_token.is_none()
|
||||
|| context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
|
||||
|
||||
let new_encrypted_device_token;
|
||||
if device_token_changed {
|
||||
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();
|
||||
|
||||
// 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?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::EncryptedDeviceToken,
|
||||
Some(&encrypted_device_token),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if encrypted_device_token_len <= 4096 {
|
||||
new_encrypted_device_token = Some(encrypted_device_token);
|
||||
} 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.");
|
||||
new_encrypted_device_token = None;
|
||||
}
|
||||
} else {
|
||||
new_encrypted_device_token = old_encrypted_device_token;
|
||||
}
|
||||
|
||||
// Store new encrypted device token on the server
|
||||
// even if it is the same as the old one.
|
||||
if let Some(encrypted_device_token) = new_encrypted_device_token {
|
||||
let folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
self.run_command_and_check_ok(&format_setmetadata(
|
||||
&folder,
|
||||
&encrypted_device_token,
|
||||
))
|
||||
.await
|
||||
.context("SETMETADATA command failed")?;
|
||||
|
||||
context.push_subscribed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
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();
|
||||
// Subscribe for heartbeat notifications.
|
||||
@@ -1667,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<()> {
|
||||
@@ -1724,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));
|
||||
}
|
||||
}
|
||||
@@ -1739,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));
|
||||
@@ -2593,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)
|
||||
@@ -2641,4 +2537,331 @@ async fn add_all_recipients_as_contacts(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod imap_tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_get_folder_meaning_by_name() {
|
||||
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("Messages envoyés"),
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
|
||||
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
|
||||
assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_uid_next_validity() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
|
||||
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sequence_sets() {
|
||||
assert_eq!(build_sequence_sets(&[]).unwrap(), vec![]);
|
||||
|
||||
let cases = vec![
|
||||
(vec![1], "1"),
|
||||
(vec![3291], "3291"),
|
||||
(vec![1, 3, 5, 7, 9, 11], "1,3,5,7,9,11"),
|
||||
(vec![1, 2, 3], "1:3"),
|
||||
(vec![1, 4, 5, 6], "1,4:6"),
|
||||
((1..=500).collect(), "1:500"),
|
||||
(vec![3, 4, 8, 9, 10, 11, 39, 50, 2], "3:4,8:11,39,50,2"),
|
||||
];
|
||||
for (input, s) in cases {
|
||||
assert_eq!(
|
||||
build_sequence_sets(&input).unwrap(),
|
||||
vec![(input, s.into())]
|
||||
);
|
||||
}
|
||||
|
||||
let has_number = |(uids, s): &(Vec<u32>, String), number| {
|
||||
uids.iter().any(|&n| n == number)
|
||||
&& s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
|
||||
};
|
||||
|
||||
let numbers: Vec<_> = (2..=500).step_by(2).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
assert!(result.len() == 1); // these UIDs fit in one set
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
|
||||
let numbers: Vec<_> = (1..=1000).step_by(3).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
let (last_uids, last_str) = result.last().unwrap();
|
||||
assert_eq!(
|
||||
last_uids.get((last_uids.len() - 2)..).unwrap(),
|
||||
&[997, 1000]
|
||||
);
|
||||
assert!(last_str.ends_with("997,1000"));
|
||||
assert!(result.len() == 2); // This time we need 2 sets
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
|
||||
let numbers: Vec<_> = (30000000..=30002500).step_by(4).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
assert_eq!(result.len(), 6);
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn check_target_folder_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
chat_msg: bool,
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
) -> Result<()> {
|
||||
println!("Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}");
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
|
||||
.await?;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
|
||||
.await?;
|
||||
t.ctx
|
||||
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
|
||||
.await?;
|
||||
|
||||
if accepted_chat {
|
||||
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
|
||||
ChatId::create_for_contact(&t.ctx, contact_id).await?;
|
||||
}
|
||||
let temp;
|
||||
|
||||
let bytes = if setupmessage {
|
||||
include_bytes!("../test-data/message/AutocryptSetupMessage.eml")
|
||||
} else {
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
Subject: foo\n\
|
||||
Message-ID: <abc@example.com>\n\
|
||||
{}\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
if outgoing {
|
||||
"From: alice@example.org\nTo: bob@example.net\n"
|
||||
} else {
|
||||
"From: bob@example.net\nTo: alice@example.org\n"
|
||||
},
|
||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
||||
);
|
||||
temp.as_bytes()
|
||||
};
|
||||
|
||||
let (headers, _) = mailparse::parse_headers(bytes)?;
|
||||
let actual = if let Some(config) =
|
||||
target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await?
|
||||
{
|
||||
t.get_config(config).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let expected = if expected_destination == folder {
|
||||
None
|
||||
} else {
|
||||
Some(expected_destination)
|
||||
};
|
||||
assert_eq!(expected, actual.as_deref(), "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// chat_msg means that the message was sent by Delta Chat
|
||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
// These are the same as above, but non-chat messages in Spam stay in Spam
|
||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "Spam"),
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "Spam"),
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_request() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_outgoing() -> Result<()> {
|
||||
// Test outgoing emails
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_imap_search_command() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"FROM "alice@example.org""#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@another.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@third.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uid_grouper() {
|
||||
// Input: sequence of (rowid: i64, uid: u32, target: String)
|
||||
// Output: sequence of (target: String, rowid_set: Vec<i64>, uid_set: String)
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]);
|
||||
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())]
|
||||
);
|
||||
|
||||
let grouper = UidGrouper::from([
|
||||
(1, 2, "INBOX".to_string()),
|
||||
(2, 2, "INBOX".to_string()),
|
||||
(3, 3, "INBOX".to_string()),
|
||||
]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use futures_lite::FutureExt;
|
||||
@@ -29,32 +29,25 @@ 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;
|
||||
}
|
||||
|
||||
if self.new_mail {
|
||||
info!(
|
||||
context,
|
||||
"Skipping IDLE in {folder:?} because there may be new mail."
|
||||
);
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
if let Ok(()) = idle_interrupt_receiver.try_recv() {
|
||||
info!(context, "Skip IDLE in {folder:?} because we got interrupt.");
|
||||
info!(context, "skip idle, got interrupt");
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
let mut handle = self.inner.idle();
|
||||
handle
|
||||
.init()
|
||||
.await
|
||||
.with_context(|| format!("IMAP IDLE protocol failed to init in folder {folder:?}"))?;
|
||||
if let Err(err) = handle.init().await {
|
||||
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
|
||||
}
|
||||
|
||||
// At this point IDLE command was sent and we received a "+ idling" response. We will now
|
||||
// read from the stream without getting any data for up to `IDLE_TIMEOUT`. If we don't
|
||||
@@ -68,10 +61,7 @@ impl Session {
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"IDLE entering wait-on-remote state in folder {folder:?}."
|
||||
);
|
||||
info!(context, "{folder}: Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
idle_interrupt_receiver.recv().await.ok();
|
||||
|
||||
@@ -83,19 +73,19 @@ impl Session {
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "{folder:?}: Idle has NewData {x:?}");
|
||||
info!(context, "{folder}: Idle has NewData {:?}", x);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(context, "{folder:?}: Idle-wait timeout or interruption.");
|
||||
info!(context, "{folder}: Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(context, "{folder:?}: Idle wait was interrupted manually.");
|
||||
info!(context, "{folder}: Idle wait was interrupted manually");
|
||||
}
|
||||
Ok(Event::Interrupt) => {
|
||||
info!(context, "{folder:?}: Idle wait was interrupted.");
|
||||
info!(context, "{folder}: Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "{folder:?}: Idle wait errored: {err:?}.");
|
||||
warn!(context, "{folder}: Idle wait errored: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_get_folder_meaning_by_name() {
|
||||
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("Messages envoyés"),
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
|
||||
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
|
||||
assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_uid_next_validity() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
|
||||
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sequence_sets() {
|
||||
assert_eq!(build_sequence_sets(&[]).unwrap(), vec![]);
|
||||
|
||||
let cases = vec![
|
||||
(vec![1], "1"),
|
||||
(vec![3291], "3291"),
|
||||
(vec![1, 3, 5, 7, 9, 11], "1,3,5,7,9,11"),
|
||||
(vec![1, 2, 3], "1:3"),
|
||||
(vec![1, 4, 5, 6], "1,4:6"),
|
||||
((1..=500).collect(), "1:500"),
|
||||
(vec![3, 4, 8, 9, 10, 11, 39, 50, 2], "3:4,8:11,39,50,2"),
|
||||
];
|
||||
for (input, s) in cases {
|
||||
assert_eq!(
|
||||
build_sequence_sets(&input).unwrap(),
|
||||
vec![(input, s.into())]
|
||||
);
|
||||
}
|
||||
|
||||
let has_number = |(uids, s): &(Vec<u32>, String), number| {
|
||||
uids.iter().any(|&n| n == number)
|
||||
&& s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
|
||||
};
|
||||
|
||||
let numbers: Vec<_> = (2..=500).step_by(2).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
assert!(result.len() == 1); // these UIDs fit in one set
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
|
||||
let numbers: Vec<_> = (1..=1000).step_by(3).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
let (last_uids, last_str) = result.last().unwrap();
|
||||
assert_eq!(
|
||||
last_uids.get((last_uids.len() - 2)..).unwrap(),
|
||||
&[997, 1000]
|
||||
);
|
||||
assert!(last_str.ends_with("997,1000"));
|
||||
assert!(result.len() == 2); // This time we need 2 sets
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
|
||||
let numbers: Vec<_> = (30000000..=30002500).step_by(4).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
assert_eq!(result.len(), 6);
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_target_folder_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
chat_msg: bool,
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
) -> Result<()> {
|
||||
println!("Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}");
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
|
||||
.await?;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
|
||||
.await?;
|
||||
t.ctx
|
||||
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
|
||||
.await?;
|
||||
|
||||
if accepted_chat {
|
||||
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
|
||||
ChatId::create_for_contact(&t.ctx, contact_id).await?;
|
||||
}
|
||||
let temp;
|
||||
|
||||
let bytes = if setupmessage {
|
||||
include_bytes!("../../test-data/message/AutocryptSetupMessage.eml")
|
||||
} else {
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
Subject: foo\n\
|
||||
Message-ID: <abc@example.com>\n\
|
||||
{}\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
if outgoing {
|
||||
"From: alice@example.org\nTo: bob@example.net\n"
|
||||
} else {
|
||||
"From: bob@example.net\nTo: alice@example.org\n"
|
||||
},
|
||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
||||
);
|
||||
temp.as_bytes()
|
||||
};
|
||||
|
||||
let (headers, _) = mailparse::parse_headers(bytes)?;
|
||||
let actual = if let Some(config) =
|
||||
target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await?
|
||||
{
|
||||
t.get_config(config).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let expected = if expected_destination == folder {
|
||||
None
|
||||
} else {
|
||||
Some(expected_destination)
|
||||
};
|
||||
assert_eq!(expected, actual.as_deref(), "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// chat_msg means that the message was sent by Delta Chat
|
||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
// These are the same as above, but non-chat messages in Spam stay in Spam
|
||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "Spam"),
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "Spam"),
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_request() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_outgoing() -> Result<()> {
|
||||
// Test outgoing emails
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_imap_search_command() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"FROM "alice@example.org""#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@another.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@third.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uid_grouper() {
|
||||
// Input: sequence of (rowid: i64, uid: u32, target: String)
|
||||
// Output: sequence of (target: String, rowid_set: Vec<i64>, uid_set: String)
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]);
|
||||
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())]
|
||||
);
|
||||
|
||||
let grouper = UidGrouper::from([
|
||||
(1, 2, "INBOX".to_string()),
|
||||
(2, 2, "INBOX".to_string()),
|
||||
(3, 3, "INBOX".to_string()),
|
||||
]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
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)"
|
||||
);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
src/imex.rs
35
src/imex.rs
@@ -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())
|
||||
|
||||
@@ -26,11 +26,12 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
/* this may require a keypair to be created. this may take a second ... */
|
||||
let setup_file_content = render_setup_file(context, &setup_code).await?;
|
||||
/* encrypting may also take a while ... */
|
||||
let setup_file_blob = BlobObject::create_and_deduplicate_from_bytes(
|
||||
let setup_file_blob = BlobObject::create(
|
||||
context,
|
||||
setup_file_content.as_bytes(),
|
||||
"autocrypt-setup-message.html",
|
||||
)?;
|
||||
setup_file_content.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
|
||||
let mut msg = Message {
|
||||
@@ -38,8 +39,6 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
msg.param
|
||||
.set(Param::Filename, "autocrypt-setup-message.html");
|
||||
msg.subject = stock_str::ac_setup_msg_subject(context).await;
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
|
||||
@@ -33,7 +33,8 @@ use std::task::Poll;
|
||||
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use futures_lite::FutureExt;
|
||||
use iroh::{Endpoint, RelayMode};
|
||||
use iroh_net::relay::RelayMode;
|
||||
use iroh_net::Endpoint;
|
||||
use tokio::fs;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -64,11 +65,11 @@ const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
|
||||
/// task use the [`Context::stop_ongoing`] mechanism.
|
||||
#[derive(Debug)]
|
||||
pub struct BackupProvider {
|
||||
/// iroh endpoint.
|
||||
/// iroh-net endpoint.
|
||||
_endpoint: Endpoint,
|
||||
|
||||
/// iroh address.
|
||||
node_addr: iroh::NodeAddr,
|
||||
/// iroh-net address.
|
||||
node_addr: iroh_net::NodeAddr,
|
||||
|
||||
/// Authentication token that should be submitted
|
||||
/// to retrieve the backup.
|
||||
@@ -161,7 +162,7 @@ impl BackupProvider {
|
||||
|
||||
async fn handle_connection(
|
||||
context: Context,
|
||||
conn: iroh::endpoint::Connecting,
|
||||
conn: iroh_net::endpoint::Connecting,
|
||||
auth_token: String,
|
||||
dbfile: Arc<TempPathGuard>,
|
||||
) -> Result<()> {
|
||||
@@ -177,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?;
|
||||
@@ -291,7 +291,7 @@ impl Future for BackupProvider {
|
||||
|
||||
pub async fn get_backup2(
|
||||
context: &Context,
|
||||
node_addr: iroh::NodeAddr,
|
||||
node_addr: iroh_net::NodeAddr,
|
||||
auth_token: String,
|
||||
) -> Result<()> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
@@ -309,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")?;
|
||||
@@ -341,7 +337,7 @@ pub async fn get_backup2(
|
||||
/// This is a long running operation which will return only when completed.
|
||||
///
|
||||
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
|
||||
/// does avoid having [`iroh::NodeAddr`] in the primary API however, without
|
||||
/// does avoid having [`iroh_net::NodeAddr`] in the primary API however, without
|
||||
/// having to revert to untyped bytes.
|
||||
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
match qr {
|
||||
@@ -393,8 +389,7 @@ mod tests {
|
||||
let file = ctx0.get_blobdir().join("hello.txt");
|
||||
fs::write(&file, "i am attachment").await.unwrap();
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_and_deduplicate(&ctx0, &file, Some("hello.txt"), Some("text/plain"))
|
||||
.unwrap();
|
||||
msg.set_file(file.to_str().unwrap(), Some("text/plain"));
|
||||
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
|
||||
|
||||
// Prepare to transfer backup.
|
||||
@@ -428,12 +423,7 @@ mod tests {
|
||||
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
|
||||
|
||||
let path = msg.get_file(&ctx1).unwrap();
|
||||
assert_eq!(
|
||||
// That's the hash of the file:
|
||||
path.with_file_name("ac1d2d284757656a8d41dc40aae4136.txt"),
|
||||
path
|
||||
);
|
||||
assert_eq!("hello.txt", msg.get_filename().unwrap());
|
||||
assert_eq!(path.with_file_name("hello.txt"), path);
|
||||
let text = fs::read_to_string(&path).await.unwrap();
|
||||
assert_eq!(text, "i am attachment");
|
||||
|
||||
@@ -444,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)]
|
||||
|
||||
64
src/key.rs
64
src/key.rs
@@ -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();
|
||||
|
||||
@@ -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())?);
|
||||
}
|
||||
@@ -1074,7 +1077,7 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
let file = alice.get_blobdir().join(file_name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("logo.png"), None)?;
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let sent = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let alice_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
|
||||
assert_eq!(alice_msg.has_location(), false);
|
||||
@@ -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?;
|
||||
|
||||
951
src/message.rs
951
src/message.rs
File diff suppressed because it is too large
Load Diff
@@ -1,757 +0,0 @@
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_guess_msgtype_from_suffix() {
|
||||
assert_eq!(
|
||||
guess_msgtype_from_path_suffix(Path::new("foo/bar-sth.mp3")),
|
||||
Some((Viewtype::Audio, "audio/mpeg"))
|
||||
);
|
||||
assert_eq!(
|
||||
guess_msgtype_from_path_suffix(Path::new("foo/file.html")),
|
||||
Some((Viewtype::File, "text/html"))
|
||||
);
|
||||
assert_eq!(
|
||||
guess_msgtype_from_path_suffix(Path::new("foo/file.xdc")),
|
||||
Some((Viewtype::Webxdc, "application/webxdc+zip"))
|
||||
);
|
||||
}
|
||||
|
||||
#[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");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "https://foo/bar");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "url");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val");
|
||||
assert_eq!(webrtc_type, VideochatType::Unknown);
|
||||
assert_eq!(url, "https://foo/bar?key=val#key=val");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("jitsi:https://j.si/foo");
|
||||
assert_eq!(webrtc_type, VideochatType::Jitsi);
|
||||
assert_eq!(url, "https://j.si/foo");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_webrtc_instance() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si/", "123");
|
||||
assert_eq!(instance, "https://meet.jit.si/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si", "456");
|
||||
assert_eq!(instance, "https://meet.jit.si/456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("meet.jit.si", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo?", "123");
|
||||
assert_eq!(instance, "https://bla.foo?123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("jitsi:bla.foo#", "456");
|
||||
assert_eq!(instance, "jitsi:https://bla.foo#456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=", "789");
|
||||
assert_eq!(instance, "https://bla.foo#room=789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://bla.foo#room", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=$ROOM&after=cont", "234");
|
||||
assert_eq!(instance, "https://bla.foo#room=234&after=cont");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" meet.jit .si ", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" basicwebrtc: basic . stuff\n ", "12345ab");
|
||||
assert_eq!(instance, "basicwebrtc:https://basic.stuff/12345ab");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_webrtc_instance_noroom() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("bla.foo$NOROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" bla . foo $NOROOM ", "456");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" $NOROOM bla . foo ", "789");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" bla.foo / $NOROOM ? a = b ", "123");
|
||||
assert_eq!(instance, "https://bla.foo/?a=b");
|
||||
|
||||
// $ROOM has a higher precedence
|
||||
let instance = Message::create_webrtc_instance("bla.foo/?$NOROOM=$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo/?$NOROOM=123");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_width_height() {
|
||||
let t = test::TestContext::new().await;
|
||||
|
||||
// test that get_width() and get_height() are returning some dimensions for images;
|
||||
// (as the device-chat contains a welcome-images, we check that)
|
||||
t.update_device_chats().await.ok();
|
||||
let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut has_image = false;
|
||||
let chatitems = chat::get_chat_msgs(&t, device_chat_id).await.unwrap();
|
||||
for chatitem in chatitems {
|
||||
if let ChatItem::Message { msg_id } = chatitem {
|
||||
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
|
||||
if msg.get_viewtype() == Viewtype::Image {
|
||||
has_image = true;
|
||||
// just check that width/height are inside some reasonable ranges
|
||||
assert!(msg.get_width() > 100);
|
||||
assert!(msg.get_height() > 100);
|
||||
assert!(msg.get_width() < 4000);
|
||||
assert!(msg.get_height() < 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(has_image);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote() {
|
||||
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_text("Quoted message".to_string());
|
||||
|
||||
// Send message, 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 = Message::load_from_db(ctx, msg_id).await.unwrap();
|
||||
assert!(!msg.rfc724_mid.is_empty());
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.set_quote(ctx, Some(&msg))
|
||||
.await
|
||||
.expect("can't set quote");
|
||||
assert_eq!(msg2.quoted_text().unwrap(), msg.get_text());
|
||||
|
||||
let quoted_msg = msg2
|
||||
.quoted_message(ctx)
|
||||
.await
|
||||
.expect("error while retrieving quoted message")
|
||||
.expect("quoted message not found");
|
||||
assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_quote() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.send_recv_accept(alice, bob, "Hi!").await;
|
||||
let msg = tcm
|
||||
.send_recv(
|
||||
alice,
|
||||
bob,
|
||||
"On 2024-08-28, Alice wrote:\n> A quote.\nNot really.",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(msg.quoted_text().is_none());
|
||||
assert!(msg.quoted_message(bob).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group chat", &[bob])
|
||||
.await;
|
||||
let sent = alice.send_text(alice_group, "Hi! I created a group").await;
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
|
||||
let bob_group = bob_received_message.chat_id;
|
||||
bob_group.accept(bob).await?;
|
||||
let sent = bob.send_text(bob_group, "Encrypted message").await;
|
||||
let alice_received_message = alice.recv_msg(&sent).await;
|
||||
assert!(alice_received_message.get_showpadlock());
|
||||
|
||||
// Alice adds contact without key so chat becomes unencrypted.
|
||||
let alice_flubby_contact_id = Contact::create(alice, "Flubby", "flubby@example.org").await?;
|
||||
add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?;
|
||||
|
||||
// Alice quotes encrypted message in unencrypted chat.
|
||||
let mut msg = Message::new_text("unencrypted".to_string());
|
||||
msg.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "...");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
// Alice replaces a quote of encrypted message with a quote of unencrypted one.
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
msg1.set_quote(alice, Some(&msg)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg1).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_chat_id() {
|
||||
// Alice receives a message that pops up as a contact request
|
||||
let alice = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check chat-id of this message
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(!msg.get_chat_id().is_special());
|
||||
assert_eq!(msg.get_text(), "hello".to_string());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_override_sender_name() {
|
||||
// send message with overridden sender name
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let contact_id = *chat::get_chat_contacts(&alice, chat.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await.unwrap();
|
||||
|
||||
let mut msg = Message::new_text("bla blubb".to_string());
|
||||
msg.set_override_sender_name(Some("over ride".to_string()));
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
assert_eq!(msg.get_sender_name(&contact), "over ride".to_string());
|
||||
assert_ne!(contact.get_display_name(), "over ride".to_string());
|
||||
chat::send_msg(&alice, chat.id, &mut msg).await.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// bob receives that message
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contact_id = *chat::get_chat_contacts(&bob, chat.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&bob, contact_id).await.unwrap();
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
assert_eq!(msg.text, "bla blubb");
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
assert_eq!(msg.get_sender_name(&contact), "over ride".to_string());
|
||||
assert_ne!(contact.get_display_name(), "over ride".to_string());
|
||||
|
||||
// explicitly check that the message does not create a mailing list
|
||||
// (mailing lists may also use `Sender:`-header)
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap();
|
||||
assert_ne!(chat.typ, Chattype::Mailinglist);
|
||||
|
||||
// Alice receives message on another device.
|
||||
let msg = alice2.recv_msg(&sent_msg).await;
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_original_msg_id() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// normal sending of messages does not have an original ID
|
||||
let one2one_chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_text(one2one_chat.id, "foo").await;
|
||||
let orig_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
|
||||
assert!(orig_msg.get_original_msg_id(&alice).await?.is_none());
|
||||
assert!(orig_msg.parent(&alice).await?.is_none());
|
||||
assert!(orig_msg.quoted_message(&alice).await?.is_none());
|
||||
|
||||
// forwarding to "Saved Messages", the message gets the original ID attached
|
||||
let self_chat = alice.get_self_chat().await;
|
||||
save_msgs(&alice, &[sent.sender_msg_id]).await?;
|
||||
let saved_msg = alice.get_last_msg_in(self_chat.get_id()).await;
|
||||
assert_ne!(saved_msg.get_id(), orig_msg.get_id());
|
||||
assert_eq!(
|
||||
saved_msg.get_original_msg_id(&alice).await?.unwrap(),
|
||||
orig_msg.get_id()
|
||||
);
|
||||
assert!(saved_msg.parent(&alice).await?.is_none());
|
||||
assert!(saved_msg.quoted_message(&alice).await?.is_none());
|
||||
|
||||
// forwarding from "Saved Messages" back to another chat, detaches original ID
|
||||
forward_msgs(&alice, &[saved_msg.get_id()], one2one_chat.get_id()).await?;
|
||||
let forwarded_msg = alice.get_last_msg_in(one2one_chat.get_id()).await;
|
||||
assert_ne!(forwarded_msg.get_id(), saved_msg.get_id());
|
||||
assert_ne!(forwarded_msg.get_id(), orig_msg.get_id());
|
||||
assert!(forwarded_msg.get_original_msg_id(&alice).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
// alice sends to bob,
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
let sent1 = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg1 = bob.recv_msg(&sent1).await;
|
||||
let bob_chat_id = msg1.chat_id;
|
||||
let sent2 = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg2 = bob.recv_msg(&sent2).await;
|
||||
assert_eq!(msg1.chat_id, msg2.chat_id);
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
// that has no effect in contact request
|
||||
markseen_msgs(&bob, vec![msg1.id, msg2.id]).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.blocked, Blocked::Request);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
bob_chat_id.accept(&bob).await.unwrap();
|
||||
|
||||
// bob sends to alice,
|
||||
// alice knows bob and messages appear in normal chat
|
||||
let msg1 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let msg2 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, alice_chat.id);
|
||||
assert_eq!(chats.get_chat_id(0)?, msg1.chat_id);
|
||||
assert_eq!(chats.get_chat_id(0)?, msg2.chat_id);
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 2);
|
||||
|
||||
// no message-ids, that should have no effect
|
||||
markseen_msgs(&alice, vec![]).await?;
|
||||
|
||||
// bad message-id, that should have no effect
|
||||
markseen_msgs(&alice, vec![MsgId::new(123456)]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 2);
|
||||
|
||||
// mark the most recent as seen
|
||||
markseen_msgs(&alice, vec![msg2.id]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 1);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
// user scrolled up - mark both as seen
|
||||
markseen_msgs(&alice, vec![msg1.id, msg2.id]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 0);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_not_downloaded_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
// A not downloaded message can be seen only if it's seen on another device.
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
// Marking the message as seen again is a no op.
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::InProgress)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Failure)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Undecipherable)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
assert!(
|
||||
!alice
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?
|
||||
);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
// Let's assume that Alice and Bob resolved the problem with encryption.
|
||||
let old_msg = msg;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, old_msg.chat_id);
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
// The message state mustn't be downgraded to `InFresh`.
|
||||
assert_eq!(msg.state, MessageState::InNoticed);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
let msg = Message::load_from_db(alice, msg.id).await?;
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let seen = true;
|
||||
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
|
||||
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_state() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
// check both get_state() functions,
|
||||
// the one requiring a id and the one requiring an object
|
||||
async fn assert_state(t: &Context, msg_id: MsgId, state: MessageState) {
|
||||
assert_eq!(msg_id.get_state(t).await.unwrap(), state);
|
||||
assert_eq!(
|
||||
Message::load_from_db(t, msg_id).await.unwrap().get_state(),
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
// check outgoing messages states on sender side
|
||||
let mut alice_msg = Message::new_text("hi!".to_string());
|
||||
assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work
|
||||
|
||||
alice_chat
|
||||
.id
|
||||
.set_draft(&alice, Some(&mut alice_msg))
|
||||
.await?;
|
||||
let mut alice_msg = alice_chat.id.get_draft(&alice).await?.unwrap();
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutDraft).await;
|
||||
|
||||
let msg_id = chat::send_msg(&alice, alice_chat.id, &mut alice_msg).await?;
|
||||
assert_eq!(msg_id, alice_msg.id);
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutPending).await;
|
||||
|
||||
let payload = alice.pop_sent_msg().await;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await;
|
||||
|
||||
set_msg_failed(&alice, &mut alice_msg, "badly failed").await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;
|
||||
|
||||
// check incoming message states on receiver side
|
||||
let bob_msg = bob.recv_msg(&payload).await;
|
||||
assert_eq!(bob_chat.id, bob_msg.chat_id);
|
||||
assert_state(&bob, bob_msg.id, MessageState::InFresh).await;
|
||||
|
||||
marknoticed_chat(&bob, bob_msg.chat_id).await?;
|
||||
assert_state(&bob, bob_msg.id, MessageState::InNoticed).await;
|
||||
|
||||
markseen_msgs(&bob, vec![bob_msg.id]).await?;
|
||||
assert_state(&bob, bob_msg.id, MessageState::InSeen).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
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,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@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 who is not the bot anymore.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <456@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello again\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), "hello again".to_string());
|
||||
assert!(!msg.is_bot());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(!contact.is_bot());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_derive_display_works_as_expected() {
|
||||
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::default());
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
|
||||
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
|
||||
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
|
||||
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
|
||||
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
|
||||
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
|
||||
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
|
||||
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
|
||||
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
|
||||
assert_eq!(
|
||||
Viewtype::VideochatInvitation,
|
||||
Viewtype::from_i32(70).unwrap()
|
||||
);
|
||||
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
|
||||
assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_quotes() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
|
||||
let sent = alice.send_text(chat.id, "> First quote").await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, "> First quote");
|
||||
assert!(received.quoted_text().is_none());
|
||||
assert!(received.quoted_message(&bob).await?.is_none());
|
||||
|
||||
let sent = alice.send_text(chat.id, "> Second quote").await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, "> Second quote");
|
||||
assert!(received.quoted_text().is_none());
|
||||
assert!(received.quoted_message(&bob).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_message_summary_text() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
let msg_id = send_text_msg(&t, chat.id, "foo".to_string()).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
let summary = msg.get_summary(&t, None).await?;
|
||||
assert_eq!(summary.text, "foo");
|
||||
|
||||
// message summary does not change when reactions are applied (in contrast to chatlist summary)
|
||||
send_reaction(&t, msg_id, "🫵").await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
let summary = msg.get_summary(&t, None).await?;
|
||||
assert_eq!(summary.text, "foo");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_format_flowed_round_trip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
|
||||
let text = " Foo bar";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let text = "Foo bar baz";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let python_program = "\
|
||||
def hello():
|
||||
return 'Hello, world!'";
|
||||
let sent = alice.send_text(chat.id, python_program).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, python_program);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_msgs_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
delete_msgs(&alice, &[msg.id]).await?;
|
||||
assert!(!alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
//! # MIME message production.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
@@ -67,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,
|
||||
@@ -155,10 +126,8 @@ fn new_address_with_name(name: &str, address: String) -> Address {
|
||||
|
||||
impl MimeFactory {
|
||||
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
|
||||
let now = time();
|
||||
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
|
||||
@@ -176,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());
|
||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
}
|
||||
to.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 if remove_timestamp.saturating_add(60 * 24 * 3600) > now {
|
||||
// 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(())
|
||||
},
|
||||
)
|
||||
@@ -311,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,
|
||||
@@ -351,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,
|
||||
@@ -378,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()));
|
||||
}
|
||||
|
||||
@@ -447,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)
|
||||
@@ -566,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
|
||||
@@ -576,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
|
||||
@@ -615,31 +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 let Loaded::Message { chat, .. } = &self.loaded {
|
||||
if chat.typ == Chattype::Group
|
||||
&& !self.member_timestamps.is_empty()
|
||||
&& !chat.member_list_is_stale(context).await?
|
||||
{
|
||||
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
|
||||
@@ -752,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 {
|
||||
@@ -1149,6 +1047,7 @@ impl MimeFactory {
|
||||
part.body(text)
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn render_message(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1470,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);
|
||||
}
|
||||
|
||||
@@ -1610,18 +1509,17 @@ pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||
.join("\r\n")
|
||||
}
|
||||
|
||||
async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder> {
|
||||
let file_name = msg.get_filename().context("msg has no file")?;
|
||||
let suffix = Path::new(&file_name)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("dat");
|
||||
|
||||
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");
|
||||
|
||||
// Get file name to use for sending. For privacy purposes, we do
|
||||
// not transfer the original filenames eg. for images; these names
|
||||
@@ -1641,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!(
|
||||
@@ -1661,14 +1563,18 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder
|
||||
),
|
||||
&suffix
|
||||
),
|
||||
_ => file_name,
|
||||
_ => msg
|
||||
.param
|
||||
.get(Param::Filename)
|
||||
.unwrap_or_else(|| blob.as_file_name())
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
/* check mimetype */
|
||||
let mimetype: mime::Mime = match msg.param.get(Param::MimeType) {
|
||||
Some(mtype) => mtype.parse()?,
|
||||
None => {
|
||||
if let Some(res) = message::guess_msgtype_from_suffix(msg) {
|
||||
if let Some(res) = message::guess_msgtype_from_suffix(blob.as_rel_path()) {
|
||||
res.1.parse()?
|
||||
} else {
|
||||
mime::APPLICATION_OCTET_STREAM
|
||||
@@ -1695,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> {
|
||||
@@ -1999,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\
|
||||
@@ -2025,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());
|
||||
@@ -2174,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();
|
||||
|
||||
@@ -2231,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();
|
||||
@@ -2261,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()
|
||||
}
|
||||
@@ -2284,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
|
||||
}
|
||||
@@ -2294,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\
|
||||
@@ -2307,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();
|
||||
|
||||
@@ -2561,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?;
|
||||
@@ -2579,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(())
|
||||
}
|
||||
@@ -2631,7 +2525,8 @@ mod tests {
|
||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||
// decoded_data to check presence of the necessary headers.
|
||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?;
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
|
||||
.await?;
|
||||
let sent = bob.send_msg(chat, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
|
||||
@@ -2648,40 +2543,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_remove_self() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let first_group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob])
|
||||
.await;
|
||||
alice.send_text(first_group, "Hi! I created a group.").await;
|
||||
remove_contact_from_chat(alice, first_group, ContactId::SELF).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
let second_group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob])
|
||||
.await;
|
||||
let sent = alice
|
||||
.send_text(second_group, "Hi! I created another group.")
|
||||
.await;
|
||||
|
||||
println!("{}", sent.payload);
|
||||
let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
mime_message.get_header(HeaderDef::ChatGroupPastMembers),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
mime_message.chat_group_member_timestamps().unwrap().len(),
|
||||
1 // There is a timestamp for Bob, not for Alice
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
2035
src/mimeparser.rs
2035
src/mimeparser.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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`.
|
||||
///
|
||||
|
||||
296
src/net/http.rs
296
src/net/http.rs
@@ -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::time;
|
||||
|
||||
/// HTTP(S) GET response.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
/// Response body.
|
||||
pub blob: Vec<u8>,
|
||||
@@ -93,142 +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_and_deduplicate_from_bytes(context, response.blob.as_slice(), "")?;
|
||||
|
||||
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 {
|
||||
@@ -275,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.
|
||||
@@ -403,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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
25
src/param.rs
25
src/param.rs
@@ -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.
|
||||
@@ -368,15 +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,
|
||||
@@ -384,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))
|
||||
@@ -540,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)]
|
||||
|
||||
@@ -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).
|
||||
@@ -24,12 +24,14 @@
|
||||
//! and `joinRealtimeChannel().send()` just like the other peers.
|
||||
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use email::Header;
|
||||
use futures_lite::StreamExt;
|
||||
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMap, RelayMode, RelayUrl, SecretKey};
|
||||
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use iroh_net::key::{PublicKey, SecretKey};
|
||||
use iroh_net::relay::{RelayMap, RelayUrl};
|
||||
use iroh_net::{relay::RelayMode, Endpoint};
|
||||
use iroh_net::{NodeAddr, NodeId};
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::env;
|
||||
@@ -52,8 +54,8 @@ const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
/// Store iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
pub struct Iroh {
|
||||
/// iroh router needed for iroh peer channels.
|
||||
pub(crate) router: iroh::protocol::Router,
|
||||
/// [Endpoint] needed for iroh peer channels.
|
||||
pub(crate) endpoint: Endpoint,
|
||||
|
||||
/// [Gossip] needed for iroh peer channels.
|
||||
pub(crate) gossip: Gossip,
|
||||
@@ -73,12 +75,7 @@ pub struct Iroh {
|
||||
impl Iroh {
|
||||
/// Notify the endpoint that the network has changed.
|
||||
pub(crate) async fn network_change(&self) {
|
||||
self.router.endpoint().network_change().await
|
||||
}
|
||||
|
||||
/// Closes the QUIC endpoint.
|
||||
pub(crate) async fn close(self) -> Result<()> {
|
||||
self.router.shutdown().await.context("Closing iroh failed")
|
||||
self.endpoint.network_change().await
|
||||
}
|
||||
|
||||
/// Join a topic and create the subscriber loop for it.
|
||||
@@ -115,8 +112,8 @@ impl Iroh {
|
||||
|
||||
// Inform iroh of potentially new node addresses
|
||||
for node_addr in &peers {
|
||||
if !node_addr.is_empty() {
|
||||
self.router.endpoint().add_node_addr(node_addr.clone())?;
|
||||
if !node_addr.info.is_empty() {
|
||||
self.endpoint.add_node_addr(node_addr.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +121,7 @@ impl Iroh {
|
||||
|
||||
let (gossip_sender, gossip_receiver) = self
|
||||
.gossip
|
||||
.subscribe_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
|
||||
.join_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
|
||||
.split();
|
||||
|
||||
let ctx = ctx.clone();
|
||||
@@ -143,10 +140,10 @@ impl Iroh {
|
||||
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
|
||||
if self.iroh_channels.read().await.get(&topic).is_some() {
|
||||
for peer in &peers {
|
||||
self.router.endpoint().add_node_addr(peer.clone())?;
|
||||
self.endpoint.add_node_addr(peer.clone())?;
|
||||
}
|
||||
|
||||
self.gossip.subscribe_with_opts(
|
||||
self.gossip.join_with_opts(
|
||||
topic,
|
||||
JoinOptions::with_bootstrap(peers.into_iter().map(|peer| peer.node_id)),
|
||||
);
|
||||
@@ -193,8 +190,8 @@ impl Iroh {
|
||||
|
||||
/// Get the iroh [NodeAddr] without direct IP addresses.
|
||||
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
|
||||
let mut addr = self.router.endpoint().node_addr().await?;
|
||||
addr.direct_addresses = BTreeSet::new();
|
||||
let mut addr = self.endpoint.node_addr().await?;
|
||||
addr.info.direct_addresses = BTreeSet::new();
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
@@ -237,7 +234,7 @@ impl Context {
|
||||
/// Create iroh endpoint and gossip.
|
||||
async fn init_peer_channels(&self) -> Result<Iroh> {
|
||||
info!(self, "Initializing peer channels.");
|
||||
let secret_key = SecretKey::generate(rand::rngs::OsRng);
|
||||
let secret_key = SecretKey::generate();
|
||||
let public_key = secret_key.public();
|
||||
|
||||
let relay_mode = if let Some(relay_url) = self
|
||||
@@ -262,22 +259,24 @@ impl Context {
|
||||
.await?;
|
||||
|
||||
// create gossip
|
||||
// Allow messages up to 128 KB in size.
|
||||
// We set the limit to 128 KiB to account for internal overhead,
|
||||
// but only guarantee 128 KB of payload to WebXDC developers.
|
||||
let my_addr = endpoint.node_addr().await?;
|
||||
let gossip_config = iroh_gossip::proto::topic::Config {
|
||||
// Allow messages up to 128 KB in size.
|
||||
// We set the limit to 128 KiB to account for internal overhead,
|
||||
// but only guarantee 128 KB of payload to WebXDC developers.
|
||||
max_message_size: 128 * 1024,
|
||||
..Default::default()
|
||||
};
|
||||
let gossip = Gossip::from_endpoint(endpoint.clone(), gossip_config, &my_addr.info);
|
||||
|
||||
let gossip = Gossip::builder()
|
||||
.max_message_size(128 * 1024)
|
||||
.spawn(endpoint.clone())
|
||||
.await?;
|
||||
// spawn endpoint loop that forwards incoming connections to the gossiper
|
||||
let context = self.clone();
|
||||
|
||||
let router = iroh::protocol::Router::builder(endpoint)
|
||||
.accept(GOSSIP_ALPN, gossip.clone())
|
||||
.spawn()
|
||||
.await?;
|
||||
// Shuts down on deltachat shutdown
|
||||
tokio::spawn(endpoint_loop(context, endpoint.clone(), gossip.clone()));
|
||||
|
||||
Ok(Iroh {
|
||||
router,
|
||||
endpoint,
|
||||
gossip,
|
||||
sequence_numbers: Mutex::new(HashMap::new()),
|
||||
iroh_channels: RwLock::new(HashMap::new()),
|
||||
@@ -286,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,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
|
||||
@@ -499,13 +478,54 @@ fn create_random_topic() -> TopicId {
|
||||
pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result<Header> {
|
||||
let topic = create_random_topic();
|
||||
insert_topic_stub(ctx, msg_id, topic).await?;
|
||||
let topic_string = BASE32_NOPAD.encode(topic.as_bytes()).to_ascii_lowercase();
|
||||
Ok(Header::new(
|
||||
HeaderDef::IrohGossipTopic.get_headername().to_string(),
|
||||
topic_string,
|
||||
topic.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn endpoint_loop(context: Context, endpoint: Endpoint, gossip: Gossip) {
|
||||
while let Some(conn) = endpoint.accept().await {
|
||||
let conn = match conn.accept() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to accept iroh connection: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
info!(context, "IROH_REALTIME: accepting iroh connection");
|
||||
let gossip = gossip.clone();
|
||||
let context = context.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = handle_connection(&context, conn, gossip).await {
|
||||
warn!(context, "IROH_REALTIME: iroh connection error: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
context: &Context,
|
||||
mut conn: iroh_net::endpoint::Connecting,
|
||||
gossip: Gossip,
|
||||
) -> anyhow::Result<()> {
|
||||
let alpn = conn.alpn().await?;
|
||||
let conn = conn.await?;
|
||||
let peer_id = iroh_net::endpoint::get_remote_node_id(&conn)?;
|
||||
|
||||
match alpn.as_slice() {
|
||||
GOSSIP_ALPN => gossip
|
||||
.handle_connection(conn)
|
||||
.await
|
||||
.context(format!("Gossip connection to {peer_id} failed"))?,
|
||||
_ => warn!(
|
||||
context,
|
||||
"Ignoring connection from {peer_id}: unsupported ALPN protocol"
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_loop(
|
||||
context: &Context,
|
||||
mut stream: iroh_gossip::net::GossipReceiver,
|
||||
@@ -580,6 +600,7 @@ mod tests {
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
@@ -605,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)
|
||||
@@ -614,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()
|
||||
@@ -639,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();
|
||||
@@ -661,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();
|
||||
@@ -692,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();
|
||||
@@ -723,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)]
|
||||
@@ -751,6 +742,7 @@ mod tests {
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
@@ -769,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)
|
||||
@@ -778,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()
|
||||
@@ -803,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();
|
||||
@@ -831,9 +811,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let bob_sequence_number = bob
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.get()
|
||||
.unwrap()
|
||||
.sequence_numbers
|
||||
.lock()
|
||||
@@ -842,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()
|
||||
@@ -853,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()
|
||||
@@ -863,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();
|
||||
@@ -884,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();
|
||||
@@ -907,9 +870,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(alice
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.get()
|
||||
.unwrap()
|
||||
.iroh_channels
|
||||
.read()
|
||||
@@ -934,6 +895,7 @@ mod tests {
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
let alice_webxdc = alice.get_last_msg().await;
|
||||
@@ -1001,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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user