mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +03:00
Compare commits
3 Commits
link2xt/gr
...
sk/add_loc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7694bc0ba8 | ||
|
|
9f1eb45acd | ||
|
|
092b6e96ea |
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.83.0
|
||||
RUSTUP_TOOLCHAIN: 1.82.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -37,10 +37,8 @@ jobs:
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
run: scripts/clippy.sh
|
||||
- name: Check with all features
|
||||
- name: Check
|
||||
run: cargo check --workspace --all-targets --all-features
|
||||
- name: Check with only default features
|
||||
run: cargo check --all-targets
|
||||
|
||||
npm_constants:
|
||||
name: Check if node constants are up to date
|
||||
@@ -97,15 +95,15 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.83.0
|
||||
rust: 1.82.0
|
||||
- os: windows-latest
|
||||
rust: 1.83.0
|
||||
rust: 1.82.0
|
||||
- os: macos-latest
|
||||
rust: 1.83.0
|
||||
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
|
||||
|
||||
104
.github/workflows/nix.yml
vendored
104
.github/workflows/nix.yml
vendored
@@ -1,104 +0,0 @@
|
||||
name: Test Nix flake
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
push:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
format:
|
||||
name: check flake formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- run: nix fmt
|
||||
|
||||
# Check that formatting does not change anything.
|
||||
- run: git diff --exit-code
|
||||
|
||||
build:
|
||||
name: nix build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
installable:
|
||||
# Ensure `nix develop` will work.
|
||||
- devShells.x86_64-linux.default
|
||||
|
||||
- deltachat-python
|
||||
- deltachat-repl
|
||||
- deltachat-repl-aarch64-linux
|
||||
- deltachat-repl-arm64-v8a-android
|
||||
- deltachat-repl-armeabi-v7a-android
|
||||
- deltachat-repl-armv6l-linux
|
||||
- deltachat-repl-armv7l-linux
|
||||
- deltachat-repl-i686-linux
|
||||
- deltachat-repl-win32
|
||||
- deltachat-repl-win64
|
||||
- deltachat-repl-x86_64-linux
|
||||
- deltachat-rpc-client
|
||||
- deltachat-rpc-server
|
||||
- deltachat-rpc-server-aarch64-linux
|
||||
- deltachat-rpc-server-aarch64-linux-wheel
|
||||
- deltachat-rpc-server-arm64-v8a-android
|
||||
- deltachat-rpc-server-armeabi-v7a-android
|
||||
- deltachat-rpc-server-armv6l-linux
|
||||
- deltachat-rpc-server-armv6l-linux-wheel
|
||||
- deltachat-rpc-server-armv7l-linux
|
||||
- deltachat-rpc-server-armv7l-linux-wheel
|
||||
- deltachat-rpc-server-i686-linux
|
||||
- deltachat-rpc-server-i686-linux-wheel
|
||||
- deltachat-rpc-server-source
|
||||
- deltachat-rpc-server-win32
|
||||
- deltachat-rpc-server-win32-wheel
|
||||
- deltachat-rpc-server-win64
|
||||
- deltachat-rpc-server-win64-wheel
|
||||
- deltachat-rpc-server-x86_64-linux
|
||||
- deltachat-rpc-server-x86_64-linux-wheel
|
||||
- docs
|
||||
- libdeltachat
|
||||
- python-docs
|
||||
|
||||
# Fails to build
|
||||
#- deltachat-repl-x86_64-android
|
||||
#- deltachat-repl-x86-android
|
||||
#- deltachat-rpc-server-x86_64-android
|
||||
#- deltachat-rpc-server-x86-android
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
name: nix build on macOS
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
installable:
|
||||
- deltachat-rpc-server-aarch64-darwin
|
||||
|
||||
# Fails to bulid
|
||||
# - deltachat-rpc-server-x86_64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
290
CHANGELOG.md
290
CHANGELOG.md
@@ -1,276 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
- Deprecate webxdc `descr` parameter ([#6255](https://github.com/deltachat/deltachat-core-rust/pull/6255)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- AEAP: Check that the old peerstate verified key fingerprint hasn't changed when removing it.
|
||||
- Add `AccountsChanged` and `AccountsItemChanged` events ([#6118](https://github.com/deltachat/deltachat-core-rust/pull/6118)).
|
||||
- Do not use format=flowed in outgoing messages ([#6256](https://github.com/deltachat/deltachat-core-rust/pull/6256)).
|
||||
- Add webxdc limits api.
|
||||
- Add href to IncomingWebxdcNotify event ([#6266](https://github.com/deltachat/deltachat-core-rust/pull/6266)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Revert treating some transient SMTP errors as permanent.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Create_status_update_record: Get rid of `notify` var.
|
||||
|
||||
### Tests
|
||||
|
||||
- Check that IncomingMsg isn't emitted for reactions.
|
||||
|
||||
## [1.151.1] - 2024-11-24
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Fix deltachat-rpc-server-source installable.
|
||||
|
||||
### CI
|
||||
|
||||
- Test building nix targets to avoid regressions.
|
||||
|
||||
## [1.151.0] - 2024-11-23
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Trim whitespace from scanned QR codes.
|
||||
- Use privacy-preserving webxdc addresses ([#6237](https://github.com/deltachat/deltachat-core-rust/pull/6237)).
|
||||
- Webxdc notify ([#6230](https://github.com/deltachat/deltachat-core-rust/pull/6230)).
|
||||
- `update.href` api ([#6248](https://github.com/deltachat/deltachat-core-rust/pull/6248)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Never notify SELF ([#6251](https://github.com/deltachat/deltachat-core-rust/pull/6251)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Use underscores in deltachat-rpc-server source package filename.
|
||||
- Remove imap_tools from dependencies ([#6238](https://github.com/deltachat/deltachat-core-rust/pull/6238)).
|
||||
- cargo: Update Rustls from 0.23.14 to 0.23.18.
|
||||
- deps: Bump curve25519-dalek from 3.2.0 to 4.1.3 in /fuzz.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Move style guide into a separate document.
|
||||
- Clarify DC_EVENT_INCOMING_WEBXDC_NOTIFY documentation ([#6249](https://github.com/deltachat/deltachat-core-rust/pull/6249)).
|
||||
|
||||
### Tests
|
||||
|
||||
- After AEAP, 1:1 chat isn't available for sending, but unprotected groups are ([#6222](https://github.com/deltachat/deltachat-core-rust/pull/6222)).
|
||||
|
||||
## [1.150.0] - 2024-11-21
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Correct `DC_CERTCK_ACCEPT_*` values and docs ([#6176](https://github.com/deltachat/deltachat-core-rust/pull/6176)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Use Rustls for connections with strict TLS ([#6186](https://github.com/deltachat/deltachat-core-rust/pull/6186)).
|
||||
- Experimental header protection for Autocrypt.
|
||||
- Tune down io-not-started info in connectivity-html.
|
||||
- Clear config cache in start_io() ([#6228](https://github.com/deltachat/deltachat-core-rust/pull/6228)).
|
||||
- Line-before-quote may be up to 120 character long instead of 80.
|
||||
- Use i.delta.chat in qr codes ([#6223](https://github.com/deltachat/deltachat-core-rust/pull/6223)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Prevent accidental wrong-password-notifications ([#6122](https://github.com/deltachat/deltachat-core-rust/pull/6122)).
|
||||
- Remove footers from "Show Full Message...".
|
||||
- `send_msg_to_smtp`: Return Ok if `smtp` row is deleted in parallel.
|
||||
- Only add "member added/removed" messages if they actually do that ([#5992](https://github.com/deltachat/deltachat-core-rust/pull/5992)).
|
||||
- Do not fail to load chatlist summary if the message got removed.
|
||||
- deltachat-jsonrpc: Do not fail `get_chatlist_items_by_entries` if the message got deleted.
|
||||
- deltachat-jsonrpc: Do not fail `get_draft` if draft is deleted.
|
||||
- `markseen_msgs`: Limit not yet downloaded messages state to `InNoticed` ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)).
|
||||
- Update state of message when fully downloading it.
|
||||
- Dont overwrite equal drafts ([#6212](https://github.com/deltachat/deltachat-core-rust/pull/6212)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Silence RUSTSEC-2024-0384.
|
||||
- cargo: Update rPGP from 0.13.2 to 0.14.0.
|
||||
- cargo: Update futures-concurrency from 7.6.1 to 7.6.2.
|
||||
- Update flake.nix ([#6200](https://github.com/deltachat/deltachat-core-rust/pull/6200))
|
||||
|
||||
### CI
|
||||
|
||||
- Ensure flake is formatted.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Scanned proxies are added and normalized.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Fix nightly clippy warnings.
|
||||
- Remove slicing from `is_file_in_use`.
|
||||
- Remove unnecessary `allow(clippy::indexing_slicing)`.
|
||||
- Don't use slicing in `remove_nonstandard_footer`.
|
||||
- Do not use slicing in `qr` module.
|
||||
- Eliminate indexing in `compute_mailinglist_name`.
|
||||
- Remove unused `allow(clippy::indexing_slicing)`.
|
||||
- Remove indexing/slicing from `remove_message_footer`.
|
||||
- Remove indexing/slicing from `squash_attachment_parts`.
|
||||
- Remove unused allow(clippy::indexing_slicing) for heuristically_parse_ndn.
|
||||
- Remove indexing/slicing from `parse_message_ids`.
|
||||
- Remove slicing from `remove_bottom_quote`.
|
||||
- Get rid of slicing in `remove_top_quote`.
|
||||
- Remove unused allow(clippy::indexing_slicing) from 'truncate'.
|
||||
- Forbid clippy::indexing_slicing.
|
||||
- Forbid clippy::string_slice.
|
||||
- Delete chat in a transaction.
|
||||
- Fix typo in `context.rs`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove all calls to print() from deltachat-rpc-client tests.
|
||||
- Reply to protected group from MUA.
|
||||
- Mark not downloaded message as seen ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)).
|
||||
- Mark `receive_imf()` as only for tests and "internals" feature ([#6235](https://github.com/deltachat/deltachat-core-rust/pull/6235)).
|
||||
|
||||
## [1.149.0] - 2024-11-05
|
||||
|
||||
### Build system
|
||||
@@ -507,7 +236,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
|
||||
|
||||
@@ -1098,7 +827,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
|
||||
|
||||
@@ -2035,7 +1764,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)).
|
||||
@@ -4752,10 +4481,14 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
|
||||
|
||||
- new qr-code type `DC_QR_WEBRTC` #1779
|
||||
|
||||
- new `dc_chatlist_get_summary2()` api #1771
|
||||
|
||||
- tweak smtp-timeout for larger mails #1782
|
||||
|
||||
- optimize read-receipts #1765
|
||||
|
||||
- Allow http scheme for DCACCOUNT URLs #1770
|
||||
|
||||
- improve tests #1769
|
||||
|
||||
- bug fixes #1766 #1772 #1773 #1775 #1776 #1777
|
||||
@@ -5500,12 +5233,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.148.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.5..v1.148.6
|
||||
[1.148.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.6..v1.148.7
|
||||
[1.149.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.7..v1.149.0
|
||||
[1.150.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.149.0..v1.150.0
|
||||
[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
|
||||
|
||||
266
CONTRIBUTING.md
266
CONTRIBUTING.md
@@ -1,6 +1,6 @@
|
||||
# Contributing to Delta Chat
|
||||
# Contributing guidelines
|
||||
|
||||
## Bug reports
|
||||
## Reporting bugs
|
||||
|
||||
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
|
||||
If the bug you found is specific to
|
||||
@@ -9,114 +9,178 @@ If the bug you found is specific to
|
||||
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
|
||||
report it to the corresponding repository.
|
||||
|
||||
## Feature proposals
|
||||
## Proposing features
|
||||
|
||||
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
|
||||
|
||||
## Code contributions
|
||||
## Contributing code
|
||||
|
||||
If you want to contribute a code, follow this guide.
|
||||
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||
|
||||
1. **Select an issue to work on.**
|
||||
|
||||
If you have an write access to the repository, assign the issue to yourself.
|
||||
Otherwise state in the comment that you are going to work on the issue
|
||||
to avoid duplicate work.
|
||||
|
||||
If the issue does not exist yet, create it first.
|
||||
|
||||
2. **Write the code.**
|
||||
|
||||
Follow the [coding conventions](STYLE.md) when writing the code.
|
||||
|
||||
3. **Commit the code.**
|
||||
|
||||
If you have write access to the repository,
|
||||
push a branch named `<username>/<feature>`
|
||||
so it is clear who is responsible for the branch,
|
||||
and open a PR proposing to merge the change.
|
||||
Otherwise fork the repository and create a branch in your fork.
|
||||
|
||||
Commit messages follow the [Conventional Commits] notation.
|
||||
We use [git-cliff] to generate the changelog from commit messages before the release.
|
||||
|
||||
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
|
||||
|
||||
The following prefix types are used:
|
||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||
- `test`: Test changes and improvements to the testing framework.
|
||||
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
|
||||
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
|
||||
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
|
||||
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
|
||||
|
||||
Release preparation commits are marked as "chore(release): prepare for X.Y.Z"
|
||||
as described in [releasing guide](RELEASE.md).
|
||||
|
||||
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
|
||||
|
||||
Alternatively, breaking changes can go into the commit description, e.g.:
|
||||
|
||||
```
|
||||
fix: Fix race condition and db corruption when a message was received during backup
|
||||
|
||||
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||
```
|
||||
|
||||
4. [**Open a Pull Request**](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||
|
||||
Refer to the corresponding issue.
|
||||
|
||||
If you intend to squash merge the PR from the web interface,
|
||||
make sure the PR title follows the conventional commits notation
|
||||
as it will end up being a commit title.
|
||||
Otherwise make sure each commit title follows the conventional commit notation.
|
||||
|
||||
5. **Make sure all CI checks succeed.**
|
||||
|
||||
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.
|
||||
|
||||
Push the necessary fixup commits or force-push to your branch if needed.
|
||||
|
||||
6. **Ask for review.**
|
||||
|
||||
Use built-in GitHub feature to request a review from suggested reviewers.
|
||||
|
||||
If you do not have write access to the repository, ask for review in the comments.
|
||||
|
||||
7. **Merge the PR.**
|
||||
|
||||
Once a PR has an approval and passes CI, it can be merged.
|
||||
|
||||
PRs from a branch created in the main repository,
|
||||
i.e. authored by those who have write access, are merged by their authors.
|
||||
|
||||
This is to ensure that PRs are merged as intended by the author,
|
||||
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
|
||||
|
||||
If you have multiple changes in one PR, do a rebase merge.
|
||||
Otherwise, you should usually do a squash merge.
|
||||
|
||||
If PR author does not have write access to the repository,
|
||||
maintainers who reviewed the PR can merge it.
|
||||
|
||||
If you do not have access to the repository and created a PR from a fork,
|
||||
ask the maintainers to merge the PR and say how it should be merged.
|
||||
|
||||
## Other ways to contribute
|
||||
|
||||
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
||||
If you have write access to the repository,
|
||||
push a branch named `<username>/<feature>`
|
||||
so it is clear who is responsible for the branch,
|
||||
and open a PR proposing to merge the change.
|
||||
Otherwise fork the repository and create a branch in your fork.
|
||||
|
||||
You can find the list of good first issues
|
||||
and a link to this guide
|
||||
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
||||
|
||||
### Coding conventions
|
||||
|
||||
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
||||
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
||||
|
||||
### SQL
|
||||
|
||||
Multi-line SQL statements should be formatted using string literals,
|
||||
for example
|
||||
```
|
||||
sql.execute(
|
||||
"CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT DEFAULT '' NOT NULL -- message text
|
||||
) STRICT",
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
|
||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||
or [`indoc!](https://docs.rs/indoc).
|
||||
Do not escape newlines like this:
|
||||
```
|
||||
sql.execute(
|
||||
"CREATE TABLE messages ( \
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, \
|
||||
text TEXT DEFAULT '' NOT NULL \
|
||||
) STRICT",
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
Escaping newlines
|
||||
is prone to errors like this if space before backslash is missing:
|
||||
```
|
||||
"SELECT foo\
|
||||
FROM bar"
|
||||
```
|
||||
Literal above results in `SELECT fooFROM bar` string.
|
||||
This style also does not allow using `--` comments.
|
||||
|
||||
---
|
||||
|
||||
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
|
||||
to make SQLite check column types.
|
||||
|
||||
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
|
||||
This avoids reuse of the row IDs and can avoid dangerous bugs
|
||||
like forwarding wrong message because the message was deleted
|
||||
and another message took its row ID.
|
||||
|
||||
Declare all new columns as `NOT NULL`
|
||||
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
|
||||
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
|
||||
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
|
||||
Use `HAVING COUNT(*) > 0` clause
|
||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
||||
|
||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
||||
an older version. Also don't change the column type, consider adding a new column with another name
|
||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
||||
keyword doesn't help here.
|
||||
|
||||
### Commit messages
|
||||
|
||||
Commit messages follow the [Conventional Commits] notation.
|
||||
We use [git-cliff] to generate the changelog from commit messages before the release.
|
||||
|
||||
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
|
||||
|
||||
The following prefix types are used:
|
||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||
- `test`: Test changes and improvements to the testing framework.
|
||||
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
|
||||
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
|
||||
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
|
||||
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
|
||||
|
||||
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
|
||||
|
||||
If you intend to squash merge the PR from the web interface,
|
||||
make sure the PR title follows the conventional commits notation
|
||||
as it will end up being a commit title.
|
||||
Otherwise make sure each commit title follows the conventional commit notation.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
|
||||
|
||||
Alternatively, breaking changes can go into the commit description, e.g.:
|
||||
|
||||
```
|
||||
fix: Fix race condition and db corruption when a message was received during backup
|
||||
|
||||
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||
```
|
||||
|
||||
#### Multiple Changes in one PR
|
||||
|
||||
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
|
||||
|
||||
[Clippy]: https://doc.rust-lang.org/clippy/
|
||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||
[git-cliff]: https://git-cliff.org/
|
||||
|
||||
### Errors
|
||||
|
||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
|
||||
capitalize it but do not add a full stop as the contexts will be separated by `:`.
|
||||
For example:
|
||||
```
|
||||
.with_context(|| format!("Unable to trash message {msg_id}"))
|
||||
```
|
||||
|
||||
All errors should be handled in one of these ways:
|
||||
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
|
||||
- With `.log_err().ok()`.
|
||||
- Bubbled up with `?`.
|
||||
|
||||
`backtrace` feature is enabled for `anyhow` crate
|
||||
and `debug = 1` option is set in the test profile.
|
||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||
and get a backtrace with line numbers in resultified tests
|
||||
which return `anyhow::Result`.
|
||||
|
||||
### Logging
|
||||
|
||||
For logging, use `info!`, `warn!` and `error!` macros.
|
||||
Log messages should be capitalized and have a full stop in the end. For example:
|
||||
```
|
||||
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||
```
|
||||
|
||||
Format anyhow errors with `{:#}` to print all the contexts like this:
|
||||
```
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
```
|
||||
|
||||
### Reviewing
|
||||
|
||||
Once a PR has an approval and passes CI, it can be merged.
|
||||
|
||||
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
|
||||
This is to ensure that PRs are merged as intended by the author,
|
||||
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
|
||||
|
||||
If you do not have access to the repository and created a PR from a fork,
|
||||
ask the maintainers to merge the PR and say how it should be merged.
|
||||
|
||||
## Other ways to contribute
|
||||
|
||||
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
||||
|
||||
581
Cargo.lock
generated
581
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.152.0"
|
||||
version = "1.149.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.81"
|
||||
rust-version = "1.77"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[profile.dev]
|
||||
@@ -43,7 +43,7 @@ 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"] }
|
||||
@@ -62,10 +62,10 @@ http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.10"
|
||||
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.25.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.28.1", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
kamadak-exif = "0.6.0"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = { workspace = true }
|
||||
mailparse = "0.15"
|
||||
@@ -76,7 +76,7 @@ num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.14.2", default-features = false }
|
||||
pgp = { version = "0.13.2", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
@@ -86,13 +86,12 @@ regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.10.0"
|
||||
rustls = { version = "0.23.19", default-features = false }
|
||||
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.21.0", default-features = false, features = ["aead-cipher-2022"] }
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
@@ -109,7 +108,7 @@ tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.7"
|
||||
webpki-roots = "0.26.6"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -150,7 +149,6 @@ harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "receive_emails"
|
||||
required-features = ["internals"]
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
@@ -174,7 +172,7 @@ deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.5.0"
|
||||
futures-lite = "2.4.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
nu-ansi-term = "0.46"
|
||||
@@ -186,7 +184,7 @@ rusqlite = "0.32"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.14.0"
|
||||
tempfile = "3.13.0"
|
||||
thiserror = "1"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.11"
|
||||
|
||||
@@ -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/)\]
|
||||
|
||||
@@ -14,8 +14,8 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||
|
||||
6. Tag the release: `git tag --annotate v1.116.0`.
|
||||
6. Tag the release: `git tag -a v1.116.0`.
|
||||
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
|
||||
98
STYLE.md
98
STYLE.md
@@ -1,98 +0,0 @@
|
||||
# Coding conventions
|
||||
|
||||
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
||||
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
||||
|
||||
[Clippy]: https://doc.rust-lang.org/clippy/
|
||||
|
||||
## SQL
|
||||
|
||||
Multi-line SQL statements should be formatted using string literals,
|
||||
for example
|
||||
```
|
||||
sql.execute(
|
||||
"CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT DEFAULT '' NOT NULL -- message text
|
||||
) STRICT",
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
|
||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||
or [`indoc!](https://docs.rs/indoc).
|
||||
Do not escape newlines like this:
|
||||
```
|
||||
sql.execute(
|
||||
"CREATE TABLE messages ( \
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, \
|
||||
text TEXT DEFAULT '' NOT NULL \
|
||||
) STRICT",
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
Escaping newlines
|
||||
is prone to errors like this if space before backslash is missing:
|
||||
```
|
||||
"SELECT foo\
|
||||
FROM bar"
|
||||
```
|
||||
Literal above results in `SELECT fooFROM bar` string.
|
||||
This style also does not allow using `--` comments.
|
||||
|
||||
---
|
||||
|
||||
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
|
||||
to make SQLite check column types.
|
||||
|
||||
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
|
||||
This avoids reuse of the row IDs and can avoid dangerous bugs
|
||||
like forwarding wrong message because the message was deleted
|
||||
and another message took its row ID.
|
||||
|
||||
Declare all new columns as `NOT NULL`
|
||||
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
|
||||
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
|
||||
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
|
||||
Use `HAVING COUNT(*) > 0` clause
|
||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
||||
|
||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
||||
an older version. Also don't change the column type, consider adding a new column with another name
|
||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
||||
keyword doesn't help here.
|
||||
|
||||
## Errors
|
||||
|
||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
|
||||
capitalize it but do not add a full stop as the contexts will be separated by `:`.
|
||||
For example:
|
||||
```
|
||||
.with_context(|| format!("Unable to trash message {msg_id}"))
|
||||
```
|
||||
|
||||
All errors should be handled in one of these ways:
|
||||
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
|
||||
- With `.log_err().ok()`.
|
||||
- Bubbled up with `?`.
|
||||
|
||||
`backtrace` feature is enabled for `anyhow` crate
|
||||
and `debug = 1` option is set in the test profile.
|
||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||
and get a backtrace with line numbers in resultified tests
|
||||
which return `anyhow::Result`.
|
||||
|
||||
## Logging
|
||||
|
||||
For logging, use `info!`, `warn!` and `error!` macros.
|
||||
Log messages should be capitalized and have a full stop in the end. For example:
|
||||
```
|
||||
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||
```
|
||||
|
||||
Format anyhow errors with `{:#}` to print all the contexts like this:
|
||||
```
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.152.0"
|
||||
version = "1.149.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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,
|
||||
@@ -1097,14 +1154,9 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The ID of the message with the webxdc instance.
|
||||
* @param json program-readable data, this is created in JS land as:
|
||||
* - `payload`: any JS object or primitive.
|
||||
* - `info`: optional informational message. Will be shown in chat and may be added as system notification.
|
||||
* note that also users that are not notified explicitly get the `info` or `summary` update shown in the chat.
|
||||
* - `document`: optional document name. shown eg. in title bar.
|
||||
* - `summary`: optional summary. shown beside app icon.
|
||||
* - `notify`: optional array of other users `selfAddr` to be notified e.g. by a sound about `info` or `summary`.
|
||||
* @param descr Deprecated, set to NULL
|
||||
* @param json program-readable data, the actual payload
|
||||
* @param descr The user-visible description of JSON data,
|
||||
* in case of a chess game, e.g. the move.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const char* json, const char* descr);
|
||||
@@ -2489,8 +2541,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* - DC_QR_PROXY with dc_lot_t::text1=address:
|
||||
* ask the user if they want to use the given proxy.
|
||||
* if so, call dc_set_config_from_qr() and restart I/O.
|
||||
* On success, dc_get_config(context, "proxy_url")
|
||||
* will contain the new proxy in normalized form as the first element.
|
||||
*
|
||||
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
|
||||
* e-mail address scanned, optionally, a draft message could be set in
|
||||
@@ -2540,15 +2590,13 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
|
||||
/**
|
||||
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
|
||||
* The QR code is compatible to the OPENPGP4FPR format
|
||||
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||
*
|
||||
* The scanning device will pass the scanned content to dc_check_qr() then;
|
||||
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
|
||||
* an out-of-band-verification can be joined using dc_join_securejoin()
|
||||
*
|
||||
* The returned text will also work as a normal https:-link,
|
||||
* so that the QR code is useful also without Delta Chat being installed
|
||||
* or can be passed to contacts through other channels.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id If set to a group-chat-id,
|
||||
@@ -3934,7 +3982,7 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
|
||||
*
|
||||
* Outgoing message states:
|
||||
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
|
||||
* the message enters this state before @ref DC_STATE_OUT_PENDING. Deprecated.
|
||||
* the message enters this state before @ref DC_STATE_OUT_PENDING.
|
||||
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
|
||||
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
|
||||
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
|
||||
@@ -4144,13 +4192,9 @@ 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.
|
||||
* - 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.
|
||||
* 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.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
@@ -4436,7 +4480,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
||||
*
|
||||
* Even when you display an icon,
|
||||
* you should still display the text of the informational message using dc_msg_get_text()
|
||||
@@ -4466,23 +4509,19 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
|
||||
|
||||
/**
|
||||
* Get link attached to an webxdc info message.
|
||||
* The info message needs to be of type DC_INFO_WEBXDC_INFO_MESSAGE.
|
||||
* 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 to set `document.location.href` in JS land.
|
||||
*
|
||||
* Webxdc apps can define the link by setting `update.href` when sending and update,
|
||||
* see dc_send_webxdc_status_update().
|
||||
* Typically, this is used for videos that are recoded by the UI before
|
||||
* they can be sent.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The info message object.
|
||||
* Not: the webxdc instance.
|
||||
* @return The link to be set to `document.location.href` in JS land.
|
||||
* Returns NULL if there is no link attached to the info message and on errors.
|
||||
* @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.
|
||||
*/
|
||||
char* dc_msg_get_webxdc_href (const dc_msg_t* msg);
|
||||
int dc_msg_is_increation (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
@@ -4635,7 +4674,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.
|
||||
*
|
||||
@@ -5497,8 +5536,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
|
||||
|
||||
@@ -5672,14 +5709,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_CERTCK_STRICT 1
|
||||
|
||||
/**
|
||||
* Accept certificates that are expired, self-signed
|
||||
* or not valid for the server hostname.
|
||||
*/
|
||||
#define DC_CERTCK_ACCEPT_INVALID 2
|
||||
|
||||
/**
|
||||
* For API compatibility only: Treat this as DC_CERTCK_ACCEPT_INVALID on reading.
|
||||
* Must not be written.
|
||||
* Accept invalid certificates, including self-signed ones
|
||||
* or having incorrect hostname.
|
||||
*/
|
||||
#define DC_CERTCK_ACCEPT_INVALID_CERTIFICATES 3
|
||||
|
||||
@@ -5719,23 +5750,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
|
||||
@@ -5756,8 +5770,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.
|
||||
@@ -5853,26 +5865,15 @@ int dc_event_get_data2_int(dc_event_t* event);
|
||||
|
||||
/**
|
||||
* Get data associated with an event object.
|
||||
* The meaning of the data depends on the event ID returned as @ref DC_EVENT constants.
|
||||
* The meaning of the data depends on the event ID
|
||||
* returned as @ref DC_EVENT constants by dc_event_get_id().
|
||||
* See also dc_event_get_data1_int() and dc_event_get_data2_int().
|
||||
*
|
||||
* @memberof dc_event_t
|
||||
* @param event Event object as returned from dc_get_next_event().
|
||||
* @return "data1" string or NULL.
|
||||
* The meaning depends on the event type associated with this event.
|
||||
* Must be freed using dc_str_unref().
|
||||
*/
|
||||
char* dc_event_get_data1_str(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* Get data associated with an event object.
|
||||
* The meaning of the data depends on the event ID returned as @ref DC_EVENT constants.
|
||||
*
|
||||
* @memberof dc_event_t
|
||||
* @param event Event object as returned from dc_get_next_event().
|
||||
* @return "data2" string or NULL.
|
||||
* The meaning depends on the event type associated with this event.
|
||||
* Must be freed using dc_str_unref().
|
||||
* @return "data2" as a string or NULL.
|
||||
* the meaning depends on the event type associated with this event.
|
||||
* Once you're done with the string, you have to unref it using dc_unref_str().
|
||||
*/
|
||||
char* dc_event_get_data2_str(dc_event_t* event);
|
||||
|
||||
@@ -6070,35 +6071,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_INCOMING_REACTION 2002
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A webxdc wants an info message or a changed summary to be notified.
|
||||
*
|
||||
* @param data1 (int) contact_id ID _and_ (char*) href.
|
||||
* - dc_event_get_data1_int() returns contact_id of the sending contact.
|
||||
* - dc_event_get_data1_str() returns the href as set to `update.href`.
|
||||
* @param data2 (int) msg_id _and_ (char*) text_to_notify.
|
||||
* - dc_event_get_data2_int() returns the msg_id,
|
||||
* referring to the webxdc-info-message, if there is any.
|
||||
* Sometimes no webxdc-info-message is added to the chat
|
||||
* and yet a notification is sent; in this case the msg_id
|
||||
* of the webxdc instance is returned.
|
||||
* - dc_event_get_data2_str() returns text_to_notify,
|
||||
* the text that shall be shown in the notification.
|
||||
* string must be passed to dc_str_unref() afterwards.
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_WEBXDC_NOTIFY 2003
|
||||
|
||||
|
||||
/**
|
||||
* There is a fresh message. Typically, the user will show an notification
|
||||
* when receiving this message.
|
||||
*
|
||||
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||
*
|
||||
* If the message is a webxdc info message,
|
||||
* dc_msg_get_parent() returns the webxdc instance the notification belongs to.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
*/
|
||||
@@ -6382,25 +6360,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
|
||||
|
||||
/**
|
||||
* Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
|
||||
*
|
||||
* This event is only emitted by the account manager.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_ACCOUNTS_CHANGED 2302
|
||||
|
||||
/**
|
||||
* Inform that an account property that might be shown in the account list changed, namely:
|
||||
* - is_configured (see dc_is_configured())
|
||||
* - displayname
|
||||
* - selfavatar
|
||||
* - private_tag
|
||||
*
|
||||
* This event is emitted from the account whose property changed.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_ACCOUNTS_ITEM_CHANGED 2303
|
||||
|
||||
/**
|
||||
* Inform that some events have been skipped due to event channel overflow.
|
||||
@@ -6848,7 +6807,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
|
||||
|
||||
|
||||
@@ -413,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,
|
||||
@@ -532,7 +542,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::MsgsChanged { .. } => 2000,
|
||||
EventType::ReactionsChanged { .. } => 2001,
|
||||
EventType::IncomingReaction { .. } => 2002,
|
||||
EventType::IncomingWebxdcNotify { .. } => 2003,
|
||||
EventType::IncomingMsg { .. } => 2005,
|
||||
EventType::IncomingMsgBunch { .. } => 2006,
|
||||
EventType::MsgsNoticed { .. } => 2008,
|
||||
@@ -559,8 +568,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
EventType::AccountsChanged => 2302,
|
||||
EventType::AccountsItemChanged => 2303,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
@@ -593,12 +600,9 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged => 0,
|
||||
EventType::IncomingReaction { contact_id, .. }
|
||||
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -670,8 +674,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
@@ -679,7 +681,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingReaction { msg_id, .. }
|
||||
| EventType::IncomingWebxdcNotify { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
@@ -699,27 +700,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_get_data1_str(event: *mut dc_event_t) -> *mut libc::c_char {
|
||||
if event.is_null() {
|
||||
eprintln!("ignoring careless call to dc_event_get_data1_str()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let event = &(*event).typ;
|
||||
|
||||
match event {
|
||||
EventType::IncomingWebxdcNotify { href, .. } => {
|
||||
if let Some(href) = href {
|
||||
href.to_c_string().unwrap_or_default().into_raw()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
_ => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut libc::c_char {
|
||||
if event.is_null() {
|
||||
@@ -768,8 +748,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
@@ -797,9 +775,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
EventType::IncomingWebxdcNotify { text, .. } => {
|
||||
text.to_c_string().unwrap_or_default().into_raw()
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
@@ -976,6 +951,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,
|
||||
@@ -1063,7 +1059,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
json: *const libc::c_char,
|
||||
_descr: *const libc::c_char,
|
||||
descr: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_webxdc_status_update()");
|
||||
@@ -1071,10 +1067,14 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ctx.send_webxdc_status_update(MsgId::new(msg_id), &to_string_lossy(json)))
|
||||
.context("Failed to send webxdc update")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
block_on(ctx.send_webxdc_status_update(
|
||||
MsgId::new(msg_id),
|
||||
&to_string_lossy(json),
|
||||
&to_string_lossy(descr),
|
||||
))
|
||||
.context("Failed to send webxdc update")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2520,8 +2520,12 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
|
||||
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
|
||||
block_on(async {
|
||||
location::is_sending_locations_to_chat(ctx, chat_id)
|
||||
.await
|
||||
.map(|res| res as u8)
|
||||
})
|
||||
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3682,14 +3686,13 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
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_get_webxdc_href()");
|
||||
return "".strdup();
|
||||
eprintln!("ignoring careless call to dc_msg_is_increation()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_webxdc_href().strdup()
|
||||
ffi_msg.message.is_increation().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.152.0"
|
||||
version = "1.149.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -1135,11 +1135,9 @@ impl CommandApi {
|
||||
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
let message_object = MessageObject::from_msg_id(&ctx, msg_id)
|
||||
MessageObject::from_msg_id(&ctx, msg_id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))?
|
||||
.with_context(|| format!("Message {msg_id} does not exist for account {account_id}"))?;
|
||||
Ok(message_object)
|
||||
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
|
||||
}
|
||||
|
||||
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
||||
@@ -1163,10 +1161,7 @@ impl CommandApi {
|
||||
messages.insert(
|
||||
message_id,
|
||||
match message_result {
|
||||
Ok(Some(message)) => MessageLoadResult::Message(message),
|
||||
Ok(None) => MessageLoadResult::LoadingError {
|
||||
error: "Message does not exist".to_string(),
|
||||
},
|
||||
Ok(message) => MessageLoadResult::Message(message),
|
||||
Err(error) => MessageLoadResult::LoadingError {
|
||||
error: format!("{error:#}"),
|
||||
},
|
||||
@@ -1767,10 +1762,10 @@ impl CommandApi {
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
update_str: String,
|
||||
_descr: Option<String>,
|
||||
description: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str)
|
||||
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1829,18 +1824,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
|
||||
@@ -2016,7 +1999,9 @@ impl CommandApi {
|
||||
async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result<Option<MessageObject>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
|
||||
Ok(MessageObject::from_msg_id(&ctx, draft.get_id()).await?)
|
||||
Ok(Some(
|
||||
MessageObject::from_msg_id(&ctx, draft.get_id()).await?,
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -2185,9 +2170,7 @@ impl CommandApi {
|
||||
.await?;
|
||||
}
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
|
||||
let message = MessageObject::from_msg_id(&ctx, msg_id)
|
||||
.await?
|
||||
.context("Just sent message does not exist")?;
|
||||
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
|
||||
Ok((msg_id.to_u32(), message))
|
||||
}
|
||||
|
||||
|
||||
@@ -88,17 +88,11 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
|
||||
let (last_updated, message_type) = match last_msgid {
|
||||
Some(id) => {
|
||||
if let Some(last_message) =
|
||||
deltachat::message::Message::load_from_db_optional(ctx, id).await?
|
||||
{
|
||||
(
|
||||
Some(last_message.get_timestamp() * 1000),
|
||||
Some(last_message.get_viewtype().into()),
|
||||
)
|
||||
} else {
|
||||
// Message may be deleted by the time we try to load it.
|
||||
(None, None)
|
||||
}
|
||||
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
|
||||
(
|
||||
Some(last_message.get_timestamp() * 1000),
|
||||
Some(last_message.get_viewtype().into()),
|
||||
)
|
||||
}
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
@@ -106,15 +106,6 @@ pub enum EventType {
|
||||
reaction: String,
|
||||
},
|
||||
|
||||
/// Incoming webxdc info or summary update, should be notified.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingWebxdcNotify {
|
||||
contact_id: u32,
|
||||
msg_id: u32,
|
||||
text: String,
|
||||
href: Option<String>,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -286,20 +277,6 @@ pub enum EventType {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatlistItemChanged { chat_id: Option<u32> },
|
||||
|
||||
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
|
||||
///
|
||||
/// This event is only emitted by the account manager
|
||||
AccountsChanged,
|
||||
|
||||
/// Inform that an account property that might be shown in the account list changed, namely:
|
||||
/// - is_configured (see is_configured())
|
||||
/// - displayname
|
||||
/// - selfavatar
|
||||
/// - private_tag
|
||||
///
|
||||
/// This event is emitted from the account whose property changed.
|
||||
AccountsItemChanged,
|
||||
|
||||
/// Inform than some events have been skipped due to event channel overflow.
|
||||
EventChannelOverflow { n: u64 },
|
||||
}
|
||||
@@ -342,17 +319,6 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
reaction: reaction.as_str().to_string(),
|
||||
},
|
||||
CoreEventType::IncomingWebxdcNotify {
|
||||
contact_id,
|
||||
msg_id,
|
||||
text,
|
||||
href,
|
||||
} => IncomingWebxdcNotify {
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
text,
|
||||
href,
|
||||
},
|
||||
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
@@ -443,8 +409,6 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
CoreEventType::AccountsChanged => AccountsChanged,
|
||||
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
|
||||
@@ -85,8 +85,6 @@ pub struct MessageObject {
|
||||
|
||||
webxdc_info: Option<WebxdcMessageInfo>,
|
||||
|
||||
webxdc_href: Option<String>,
|
||||
|
||||
download_state: DownloadState,
|
||||
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
@@ -114,10 +112,8 @@ enum MessageQuote {
|
||||
}
|
||||
|
||||
impl MessageObject {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Option<Self>> {
|
||||
let Some(message) = Message::load_from_db_optional(context, msg_id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
let sender_contact = Contact::get_by_id(context, message.get_from_id())
|
||||
.await
|
||||
@@ -187,7 +183,7 @@ impl MessageObject {
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
let message_object = MessageObject {
|
||||
Ok(MessageObject {
|
||||
id: msg_id.to_u32(),
|
||||
chat_id: message.get_chat_id().to_u32(),
|
||||
from_id: message.get_from_id().to_u32(),
|
||||
@@ -243,17 +239,12 @@ 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,
|
||||
|
||||
vcard_contact: vcard_contacts.first().cloned(),
|
||||
};
|
||||
Ok(Some(message_object))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,161 +6,87 @@ use typescript_type_def::TypeDef;
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum QrObject {
|
||||
/// Ask the user whether to verify the contact.
|
||||
///
|
||||
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
|
||||
AskVerifyContact {
|
||||
/// ID of the contact.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user whether to join the group.
|
||||
AskVerifyGroup {
|
||||
/// Group name.
|
||||
grpname: String,
|
||||
/// Group ID.
|
||||
grpid: String,
|
||||
/// ID of the contact.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Contact fingerprint is verified.
|
||||
///
|
||||
/// Ask the user if they want to start chatting.
|
||||
FprOk {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
},
|
||||
/// Scanned fingerprint does not match the last seen fingerprint.
|
||||
FprMismatch {
|
||||
/// Contact ID.
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
/// The scanned QR code contains a fingerprint but no e-mail address.
|
||||
FprWithoutAddr {
|
||||
/// Key fingerprint.
|
||||
fingerprint: String,
|
||||
},
|
||||
/// Ask the user if they want to create an account on the given domain.
|
||||
Account {
|
||||
/// Server domain name.
|
||||
domain: String,
|
||||
},
|
||||
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
|
||||
Backup2 {
|
||||
/// Authentication token.
|
||||
auth_token: String,
|
||||
/// Iroh node address.
|
||||
|
||||
node_addr: String,
|
||||
},
|
||||
/// Ask the user if they want to use the given service for video chats.
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
/// Ask the user if they want to use the given proxy.
|
||||
///
|
||||
/// Note that HTTP(S) URLs without a path
|
||||
/// and query parameters are treated as HTTP(S) proxy URL.
|
||||
/// UI may want to still offer to open the URL
|
||||
/// in the browser if QR code contents
|
||||
/// starts with `http://` or `https://`
|
||||
/// and the QR code was not scanned from
|
||||
/// the proxy configuration screen.
|
||||
Proxy {
|
||||
/// Proxy URL.
|
||||
///
|
||||
/// This is the URL that is going to be added.
|
||||
url: String,
|
||||
/// Host extracted from the URL to display in the UI.
|
||||
host: String,
|
||||
/// Port extracted from the URL to display in the UI.
|
||||
port: u16,
|
||||
},
|
||||
/// Contact address is scanned.
|
||||
///
|
||||
/// Optionally, a draft message could be provided.
|
||||
/// Ask the user if they want to start chatting.
|
||||
Addr {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Draft message.
|
||||
draft: Option<String>,
|
||||
},
|
||||
/// URL scanned.
|
||||
///
|
||||
/// Ask the user if they want to open a browser or copy the URL to clipboard.
|
||||
Url { url: String },
|
||||
/// Text scanned.
|
||||
///
|
||||
/// Ask the user if they want to copy the text to clipboard.
|
||||
Text { text: String },
|
||||
/// Ask the user if they want to withdraw their own QR code.
|
||||
Url {
|
||||
url: String,
|
||||
},
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
WithdrawVerifyContact {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to withdraw their own group invite QR code.
|
||||
WithdrawVerifyGroup {
|
||||
/// Group name.
|
||||
grpname: String,
|
||||
/// Group ID.
|
||||
grpid: String,
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to revive their own QR code.
|
||||
ReviveVerifyContact {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to revive their own group invite QR code.
|
||||
ReviveVerifyGroup {
|
||||
/// Contact ID.
|
||||
grpname: String,
|
||||
/// Group ID.
|
||||
grpid: String,
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// `dclogin:` scheme parameters.
|
||||
///
|
||||
/// Ask the user if they want to login with the email address.
|
||||
Login { address: String },
|
||||
Login {
|
||||
address: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
@@ -215,6 +141,7 @@ impl From<Qr> for QrObject {
|
||||
auth_token,
|
||||
} => QrObject::Backup2 {
|
||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
||||
|
||||
auth_token,
|
||||
},
|
||||
Qr::WebrtcInstance {
|
||||
|
||||
@@ -35,14 +35,6 @@ pub struct WebxdcMessageInfo {
|
||||
source_code_url: Option<String>,
|
||||
/// True if full internet access should be granted to the app.
|
||||
internet_access: bool,
|
||||
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
self_addr: String,
|
||||
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
||||
send_update_interval: usize,
|
||||
/// Maximum number of bytes accepted for a serialized update object.
|
||||
/// Should be exposed to `window.sendUpdateMaxSize` in JS land.
|
||||
send_update_max_size: usize,
|
||||
}
|
||||
|
||||
impl WebxdcMessageInfo {
|
||||
@@ -57,11 +49,7 @@ impl WebxdcMessageInfo {
|
||||
document,
|
||||
summary,
|
||||
source_code_url,
|
||||
request_integration: _,
|
||||
internet_access,
|
||||
self_addr,
|
||||
send_update_interval,
|
||||
send_update_max_size,
|
||||
} = message.get_webxdc_info(context).await?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -71,9 +59,6 @@ impl WebxdcMessageInfo {
|
||||
summary: maybe_empty_string_to_option(summary),
|
||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||
internet_access,
|
||||
self_addr,
|
||||
send_update_interval,
|
||||
send_update_max_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
pub mod api;
|
||||
pub use yerpc;
|
||||
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.152.0"
|
||||
"version": "1.149.0"
|
||||
}
|
||||
|
||||
@@ -90,11 +90,6 @@ impl Ratelimit {
|
||||
pub fn until_can_send(&self) -> Duration {
|
||||
self.until_can_send_at(SystemTime::now())
|
||||
}
|
||||
|
||||
/// Returns minimum possible update interval in milliseconds.
|
||||
pub fn update_interval(&self) -> usize {
|
||||
(self.window.as_millis() as f64 / self.quota) as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -107,7 +102,6 @@ mod tests {
|
||||
|
||||
let mut ratelimit = Ratelimit::new_at(Duration::new(60, 0), 3.0, now);
|
||||
assert!(ratelimit.can_send_at(now));
|
||||
assert_eq!(ratelimit.update_interval(), 20_000);
|
||||
|
||||
// Send burst of 3 messages.
|
||||
ratelimit.send_at(now);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.152.0"
|
||||
version = "1.149.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
@@ -13,7 +13,7 @@ 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"] }
|
||||
|
||||
|
||||
@@ -629,7 +629,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending_locations_to_chat(&context, None).await? {
|
||||
if location::is_sending_locations_to_chat(&context, None).await?
|
||||
!= location::LocationSendingStatus::Disabled
|
||||
{
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
@@ -841,7 +843,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
|
||||
println!(
|
||||
"Location streaming: {}",
|
||||
"Location streaming: {:?}",
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
@@ -969,7 +971,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"Arguments <msg-id> <json status update> expected"
|
||||
);
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
context.send_webxdc_status_update(msg_id, arg2).await?;
|
||||
context
|
||||
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
|
||||
.await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ $ pip install .
|
||||
## Testing
|
||||
|
||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
2. Install tox `pip install -U tox`
|
||||
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.152.0"
|
||||
version = "1.149.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -24,6 +24,9 @@ classifiers = [
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"imap-tools",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
deltachat_rpc_client = [
|
||||
|
||||
@@ -61,8 +61,6 @@ class EventType(str, Enum):
|
||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
Internal Python-level IMAP handling used by the tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import imaplib
|
||||
@@ -7,11 +11,17 @@ import ssl
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from imap_tools import AND, Header, MailBox, MailMessage, MailMessageFlags, errors
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client import Account
|
||||
from . import Account
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
@@ -19,8 +29,6 @@ ALL = "1:*"
|
||||
|
||||
|
||||
class DirectImap:
|
||||
"""Internal Python-level IMAP handling."""
|
||||
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
@@ -204,8 +212,3 @@ class IdleManager:
|
||||
def done(self):
|
||||
"""send idle-done to server if we are currently in idle mode."""
|
||||
return self.direct_imap.conn.idle.stop()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def direct_imap():
|
||||
return DirectImap
|
||||
@@ -1,29 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
|
||||
def test_event_on_configuration(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
||||
"""
|
||||
|
||||
account = acfactory.new_preconfigured_account()
|
||||
account.clear_all_events()
|
||||
assert not account.is_configured()
|
||||
future = account.configure.future()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
|
||||
break
|
||||
assert account.is_configured()
|
||||
|
||||
future()
|
||||
|
||||
|
||||
# other tests are written in rust: src/tests/account_events.rs
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.direct_imap import DirectImap
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -535,7 +536,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
def test_reactions_for_a_reordering_move(acfactory):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
@@ -559,7 +560,7 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap = DirectImap(ac2)
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
@@ -24,9 +24,6 @@ def test_webxdc(acfactory) -> None:
|
||||
"name": "Chess Board",
|
||||
"sourceCodeUrl": None,
|
||||
"summary": None,
|
||||
"selfAddr": webxdc_info["selfAddr"],
|
||||
"sendUpdateInterval": 1000,
|
||||
"sendUpdateMaxSize": 18874368,
|
||||
}
|
||||
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
|
||||
@@ -16,7 +16,6 @@ deps =
|
||||
pytest
|
||||
pytest-timeout
|
||||
pytest-xdist
|
||||
imap-tools
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.152.0"
|
||||
version = "1.149.0"
|
||||
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.152.0"
|
||||
"version": "1.149.0"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
10
deny.toml
10
deny.toml
@@ -15,12 +15,6 @@ ignore = [
|
||||
# Unmaintained proc-macro-error
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2024-0370>
|
||||
"RUSTSEC-2024-0370",
|
||||
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# idna 0.5.0
|
||||
"RUSTSEC-2024-0421",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -36,11 +30,9 @@ skip = [
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ 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 = "idna", version = "0.5.0" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
@@ -52,7 +44,6 @@ skip = [
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "unicode-width", version = "0.1.11" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.52" },
|
||||
@@ -81,7 +72,6 @@ allow = [
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
61
flake.lock
generated
61
flake.lock
generated
@@ -7,11 +7,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731356359,
|
||||
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
|
||||
"lastModified": 1729628358,
|
||||
"narHash": "sha256-2HDSc6BL+bE3S1l3Gn0Z8wWvvfBEUEjvXkNIQ11Aifk=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
|
||||
"rev": "52b9e0c0f9cff887d2bb4932f8be4e062ba0802d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -47,11 +47,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731393059,
|
||||
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
|
||||
"lastModified": 1714112748,
|
||||
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
|
||||
"rev": "3ae4b908a795b6a3824d401a0702e11a7157d7e1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -83,11 +83,11 @@
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -101,11 +101,11 @@
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1721727458,
|
||||
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
|
||||
"lastModified": 1713520724,
|
||||
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
|
||||
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -116,11 +116,11 @@
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1730207686,
|
||||
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=",
|
||||
"lastModified": 1710156097,
|
||||
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4",
|
||||
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -131,11 +131,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
"lastModified": 1729413321,
|
||||
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -147,11 +147,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
"lastModified": 1713895582,
|
||||
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -163,9 +163,10 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 0,
|
||||
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
|
||||
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
|
||||
"lastModified": 1711668574,
|
||||
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
|
||||
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
|
||||
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
@@ -175,11 +176,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
"lastModified": 1729413321,
|
||||
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -202,11 +203,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1731342671,
|
||||
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
|
||||
"lastModified": 1714031783,
|
||||
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
|
||||
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
50
flake.nix
50
flake.nix
@@ -363,8 +363,6 @@
|
||||
mkRustPackages "x86_64-linux" //
|
||||
mkRustPackages "armv7l-linux" //
|
||||
mkRustPackages "armv6l-linux" //
|
||||
mkRustPackages "x86_64-darwin" //
|
||||
mkRustPackages "aarch64-darwin" //
|
||||
mkAndroidPackages "armeabi-v7a" //
|
||||
mkAndroidPackages "arm64-v8a" //
|
||||
mkAndroidPackages "x86" //
|
||||
@@ -481,8 +479,8 @@
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat_rpc_server-${manifest.version}.tar.gz'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-${manifest.version}.tar.gz $out'';
|
||||
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
|
||||
};
|
||||
|
||||
deltachat-rpc-client =
|
||||
@@ -535,30 +533,28 @@
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default =
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
"rustc"
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
];
|
||||
devShells.default = let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
"rustc"
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
//! is assumed to be set to "no".
|
||||
//!
|
||||
//! For received messages, DelSp parameter is honoured.
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
|
||||
/// Wraps line to 72 characters using format=flowed soft breaks.
|
||||
///
|
||||
|
||||
2609
fuzz/Cargo.lock
generated
2609
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
DC_CERTCK_ACCEPT_INVALID: 2,
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
|
||||
DC_CERTCK_AUTO: 0,
|
||||
DC_CERTCK_STRICT: 1,
|
||||
@@ -31,8 +30,6 @@ module.exports = {
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
|
||||
DC_EVENT_ACCOUNTS_CHANGED: 2302,
|
||||
DC_EVENT_ACCOUNTS_ITEM_CHANGED: 2303,
|
||||
DC_EVENT_CHANNEL_OVERFLOW: 2400,
|
||||
DC_EVENT_CHATLIST_CHANGED: 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
|
||||
@@ -54,7 +51,6 @@ module.exports = {
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
|
||||
DC_EVENT_INCOMING_REACTION: 2002,
|
||||
DC_EVENT_INCOMING_WEBXDC_NOTIFY: 2003,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
|
||||
@@ -17,7 +17,6 @@ module.exports = {
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
@@ -44,7 +43,5 @@ module.exports = {
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2302: 'DC_EVENT_ACCOUNTS_CHANGED',
|
||||
2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Generated!
|
||||
|
||||
export enum C {
|
||||
DC_CERTCK_ACCEPT_INVALID = 2,
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
|
||||
DC_CERTCK_AUTO = 0,
|
||||
DC_CERTCK_STRICT = 1,
|
||||
@@ -31,8 +30,6 @@ export enum C {
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
|
||||
DC_EVENT_ACCOUNTS_CHANGED = 2302,
|
||||
DC_EVENT_ACCOUNTS_ITEM_CHANGED = 2303,
|
||||
DC_EVENT_CHANNEL_OVERFLOW = 2400,
|
||||
DC_EVENT_CHATLIST_CHANGED = 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
|
||||
@@ -54,7 +51,6 @@ export enum C {
|
||||
DC_EVENT_INCOMING_MSG = 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
|
||||
DC_EVENT_INCOMING_REACTION = 2002,
|
||||
DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003,
|
||||
DC_EVENT_INFO = 100,
|
||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||
DC_EVENT_MSGS_CHANGED = 2000,
|
||||
@@ -328,7 +324,6 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
@@ -355,7 +350,5 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2302: 'DC_EVENT_ACCOUNTS_CHANGED',
|
||||
2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.152.0"
|
||||
"version": "1.149.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.152.0"
|
||||
version = "1.149.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
|
||||
@@ -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}).*")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1366,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
|
||||
@@ -1898,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",
|
||||
)
|
||||
|
||||
@@ -1911,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"
|
||||
@@ -1927,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")
|
||||
@@ -1937,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.
|
||||
@@ -2290,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")
|
||||
@@ -680,12 +705,27 @@ class TestOfflineChat:
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
assert qr.startswith("https://i.delta.chat")
|
||||
assert qr.startswith("OPENPGP4FPR:")
|
||||
res = ac2.check_qr(qr)
|
||||
assert res.is_ask_verifycontact()
|
||||
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")
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-12-12
|
||||
2024-11-05
|
||||
@@ -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.83.0
|
||||
RUST_VERSION=1.82.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -25,7 +25,7 @@ def build_source_package(version, filename):
|
||||
|
||||
def pack(name, contents):
|
||||
contents = contents.encode()
|
||||
tar_info = tarfile.TarInfo(f"deltachat_rpc_server-{version}/{name}")
|
||||
tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}")
|
||||
tar_info.mode = 0o644
|
||||
tar_info.size = len(contents)
|
||||
pkg.addfile(tar_info, BytesIO(contents))
|
||||
@@ -167,7 +167,7 @@ def main():
|
||||
cargo_manifest = tomllib.load(fp)
|
||||
version = cargo_manifest["package"]["version"]
|
||||
if sys.argv[1] == "source":
|
||||
filename = f"deltachat_rpc_server-{version}.tar.gz"
|
||||
filename = f"deltachat-rpc-server-{version}.tar.gz"
|
||||
build_source_package(version, filename)
|
||||
else:
|
||||
arch = sys.argv[1]
|
||||
|
||||
@@ -139,7 +139,6 @@ impl Accounts {
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
@@ -157,7 +156,6 @@ impl Accounts {
|
||||
.build()
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
@@ -192,7 +190,6 @@ impl Accounts {
|
||||
.context("failed to remove account data")?;
|
||||
}
|
||||
self.config.remove_account(id).await?;
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -260,6 +260,7 @@ fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
|
||||
35
src/blob.rs
35
src/blob.rs
@@ -763,6 +763,7 @@ mod tests {
|
||||
use fs::File;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
@@ -1455,4 +1456,38 @@ mod tests {
|
||||
check_image_size(file_saved, width, height);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_increation_in_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let file = t.get_blobdir().join("anyfile.dat");
|
||||
fs::write(&file, b"bla").await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
|
||||
assert_eq!(prepared_id, msg.id);
|
||||
assert!(msg.is_increation());
|
||||
|
||||
let msg = Message::load_from_db(&t, prepared_id).await?;
|
||||
assert!(msg.is_increation());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_increation_not_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
|
||||
|
||||
let file = t.dir.path().join("anyfile.dat");
|
||||
fs::write(&file, b"bla").await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
524
src/chat.rs
524
src/chat.rs
@@ -312,7 +312,7 @@ impl ChatId {
|
||||
|
||||
/// Create a group or mailinglist raw database record with the given parameters.
|
||||
/// The function does not add SELF nor checks if the record already exists.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn create_multiuser_record(
|
||||
context: &Context,
|
||||
chattype: Chattype,
|
||||
@@ -567,14 +567,6 @@ impl ChatId {
|
||||
contact_id: Option<ContactId>,
|
||||
timestamp_sort: i64,
|
||||
) -> Result<()> {
|
||||
if contact_id == Some(ContactId::SELF) {
|
||||
// Do not add protection messages to Saved Messages chat.
|
||||
// This chat never gets protected and unprotected,
|
||||
// we do not want the first message
|
||||
// to be a protection message with an arbitrary timestamp.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let text = context.stock_protection_msg(protect, contact_id).await;
|
||||
let cmd = match protect {
|
||||
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
|
||||
@@ -773,19 +765,27 @@ impl ChatId {
|
||||
);
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
transaction.execute(
|
||||
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
|
||||
(self,),
|
||||
)?;
|
||||
transaction.execute("DELETE FROM msgs WHERE chat_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM chats WHERE id=?", (self,))?;
|
||||
Ok(())
|
||||
})
|
||||
.execute("DELETE FROM msgs WHERE chat_id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
@@ -888,7 +888,7 @@ impl ChatId {
|
||||
_ => {
|
||||
let blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context)
|
||||
.get_blob(Param::File, context, !msg.is_increation())
|
||||
.await?
|
||||
.context("no file stored in params")?;
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
@@ -922,27 +922,24 @@ impl ChatId {
|
||||
&& old_draft.chat_id == self
|
||||
&& old_draft.state == MessageState::OutDraft
|
||||
{
|
||||
let affected_rows = context
|
||||
.sql.execute(
|
||||
"UPDATE msgs
|
||||
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
|
||||
WHERE id=?7
|
||||
AND (type <> ?2
|
||||
OR txt <> ?3
|
||||
OR txt_normalized <> ?4
|
||||
OR param <> ?5
|
||||
OR mime_in_reply_to <> ?6);",
|
||||
(
|
||||
time(),
|
||||
msg.viewtype,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
msg.id,
|
||||
),
|
||||
).await?;
|
||||
return Ok(affected_rows > 0);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET timestamp=?,type=?,txt=?,txt_normalized=?,param=?,mime_in_reply_to=?
|
||||
WHERE id=?;",
|
||||
(
|
||||
time(),
|
||||
msg.viewtype,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1399,7 +1396,7 @@ impl ChatId {
|
||||
///
|
||||
/// `message_timestamp` should be either the message "sent" timestamp or a timestamp of the
|
||||
/// corresponding event in case of a system message (usually the current system time).
|
||||
/// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
|
||||
/// `always_sort_to_bottom` makes this ajust the returned timestamp up so that the message goes
|
||||
/// to the chat bottom.
|
||||
/// `received` -- whether the message is received. Otherwise being sent.
|
||||
/// `incoming` -- whether the message is incoming.
|
||||
@@ -2104,19 +2101,13 @@ impl Chat {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let new_mime_headers = new_mime_headers.map(|s| new_html_mimepart(s).build().as_string());
|
||||
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
|
||||
// We need to add some headers so that they are stripped before formatting HTML by
|
||||
// `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's
|
||||
// anyway a useful metadata about the stored text.
|
||||
true => Some(
|
||||
"Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text + "\r\n",
|
||||
),
|
||||
true => Some(msg.text.clone()),
|
||||
false => None,
|
||||
});
|
||||
let new_mime_headers = match new_mime_headers {
|
||||
Some(h) => Some(tokio::task::block_in_place(move || {
|
||||
buf_compress(h.as_bytes())
|
||||
buf_compress(new_html_mimepart(h).build().as_string().as_bytes())
|
||||
})?),
|
||||
None => None,
|
||||
};
|
||||
@@ -2216,9 +2207,7 @@ impl Chat {
|
||||
msg.id = MsgId::new(u32::try_from(raw_id)?);
|
||||
|
||||
maybe_set_logging_xdc(context, msg, self.id).await?;
|
||||
context
|
||||
.update_webxdc_integration_database(msg, context)
|
||||
.await?;
|
||||
context.update_webxdc_integration_database(msg).await?;
|
||||
}
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
Ok(msg.id)
|
||||
@@ -2607,12 +2596,10 @@ impl ChatIdBlocked {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let protected = contact_id == ContactId::SELF || {
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
peerstate.is_some_and(|p| {
|
||||
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
|
||||
})
|
||||
};
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
let protected = peerstate.map_or(false, |p| {
|
||||
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
|
||||
});
|
||||
let smeared_time = create_smeared_timestamp(context);
|
||||
|
||||
let chat_id = context
|
||||
@@ -2677,13 +2664,26 @@ impl ChatIdBlocked {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepares a message for sending.
|
||||
pub async fn prepare_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"Cannot prepare message for special chat"
|
||||
);
|
||||
|
||||
let msg_id = prepare_msg_common(context, chat_id, msg, MessageState::OutPreparing).await?;
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
|
||||
// the caller should check if the message text is empty
|
||||
} else if msg.viewtype.has_file() {
|
||||
let mut blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context)
|
||||
.get_blob(Param::File, context, !msg.is_increation())
|
||||
.await?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
let send_as_is = msg.viewtype == Viewtype::File;
|
||||
@@ -2758,92 +2758,13 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether a contact is in a chat or not.
|
||||
pub async fn is_contact_in_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
// this function works for group and for normal chats, however, it is more useful
|
||||
// for group chats.
|
||||
// ContactId::SELF may be used to check, if the user itself is in a group
|
||||
// chat (ContactId::SELF is not added to normal chats)
|
||||
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
|
||||
(chat_id, contact_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Sends a message object to a chat.
|
||||
///
|
||||
/// Sends the event #DC_EVENT_MSGS_CHANGED on success.
|
||||
/// However, this does not imply, the message really reached the recipient -
|
||||
/// sending may be delayed eg. due to network problems. However, from your
|
||||
/// view, you're done with the message. Sooner or later it will find its way.
|
||||
pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"chat_id cannot be a special chat: {chat_id}"
|
||||
);
|
||||
|
||||
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
|
||||
// protect all system messages against RTLO attacks
|
||||
if msg.is_system_message() {
|
||||
msg.text = sanitize_bidi_characters(&msg.text);
|
||||
}
|
||||
|
||||
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
|
||||
if !msg.hidden {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
}
|
||||
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
context.emit_location_changed(Some(ContactId::SELF)).await?;
|
||||
}
|
||||
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
/// Tries to send a message synchronously.
|
||||
///
|
||||
/// Creates jobs in the `smtp` table, then drectly opens an SMTP connection and sends the
|
||||
/// message. If this fails, the jobs remain in the database for later sending.
|
||||
pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
let rowids = prepare_send_msg(context, chat_id, msg).await?;
|
||||
if rowids.is_empty() {
|
||||
return Ok(msg.id);
|
||||
}
|
||||
let mut smtp = crate::smtp::Smtp::new();
|
||||
for rowid in rowids {
|
||||
send_msg_to_smtp(context, &mut smtp, rowid)
|
||||
.await
|
||||
.context("failed to send message, queued for later sending")?;
|
||||
}
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
/// Prepares a message to be sent out.
|
||||
///
|
||||
/// Returns row ids of the `smtp` table.
|
||||
async fn prepare_send_msg(
|
||||
async fn prepare_msg_common(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
msg: &mut Message,
|
||||
) -> Result<Vec<i64>> {
|
||||
change_state_to: MessageState,
|
||||
) -> Result<MsgId> {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
// Check if the chat can be sent to.
|
||||
@@ -2887,7 +2808,7 @@ async fn prepare_send_msg(
|
||||
};
|
||||
|
||||
// ... then change the MessageState in the message object
|
||||
msg.state = MessageState::OutPending;
|
||||
msg.state = change_state_to;
|
||||
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
if !msg.hidden {
|
||||
@@ -2903,6 +2824,125 @@ async fn prepare_send_msg(
|
||||
.await?;
|
||||
msg.chat_id = chat_id;
|
||||
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
/// Returns whether a contact is in a chat or not.
|
||||
pub async fn is_contact_in_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
// this function works for group and for normal chats, however, it is more useful
|
||||
// for group chats.
|
||||
// ContactId::SELF may be used to check, if the user itself is in a group
|
||||
// chat (ContactId::SELF is not added to normal chats)
|
||||
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
|
||||
(chat_id, contact_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Sends a message object to a chat.
|
||||
///
|
||||
/// Sends the event #DC_EVENT_MSGS_CHANGED on success.
|
||||
/// However, this does not imply, the message really reached the recipient -
|
||||
/// sending may be delayed eg. due to network problems. However, from your
|
||||
/// view, you're done with the message. Sooner or later it will find its way.
|
||||
// TODO: Do not allow ChatId to be 0, if prepare_msg had been called
|
||||
// the caller can get it from msg.chat_id. Forwards would need to
|
||||
// be fixed for this somehow too.
|
||||
pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
if chat_id.is_unset() {
|
||||
let forwards = msg.param.get(Param::PrepForwards);
|
||||
if let Some(forwards) = forwards {
|
||||
for forward in forwards.split(' ') {
|
||||
if let Ok(msg_id) = forward.parse::<u32>().map(MsgId::new) {
|
||||
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
|
||||
send_msg_inner(context, chat_id, &mut msg).await?;
|
||||
};
|
||||
}
|
||||
}
|
||||
msg.param.remove(Param::PrepForwards);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
return send_msg_inner(context, chat_id, msg).await;
|
||||
}
|
||||
|
||||
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
send_msg_inner(context, chat_id, msg).await
|
||||
}
|
||||
|
||||
/// Tries to send a message synchronously.
|
||||
///
|
||||
/// Creates jobs in the `smtp` table, then drectly opens an SMTP connection and sends the
|
||||
/// message. If this fails, the jobs remain in the database for later sending.
|
||||
pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
let rowids = prepare_send_msg(context, chat_id, msg).await?;
|
||||
if rowids.is_empty() {
|
||||
return Ok(msg.id);
|
||||
}
|
||||
let mut smtp = crate::smtp::Smtp::new();
|
||||
for rowid in rowids {
|
||||
send_msg_to_smtp(context, &mut smtp, rowid)
|
||||
.await
|
||||
.context("failed to send message, queued for later sending")?;
|
||||
}
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
// protect all system messages against RTLO attacks
|
||||
if msg.is_system_message() {
|
||||
msg.text = sanitize_bidi_characters(&msg.text);
|
||||
}
|
||||
|
||||
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
|
||||
if !msg.hidden {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
}
|
||||
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
context.emit_location_changed(Some(ContactId::SELF)).await?;
|
||||
}
|
||||
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
/// Returns row ids of the `smtp` table.
|
||||
async fn prepare_send_msg(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
msg: &mut Message,
|
||||
) -> Result<Vec<i64>> {
|
||||
// prepare_msg() leaves the message state to OutPreparing, we
|
||||
// only have to change the state to OutPending in this case.
|
||||
// Otherwise we still have to prepare the message, which will set
|
||||
// the state to OutPending.
|
||||
if msg.state != MessageState::OutPreparing {
|
||||
// automatically prepare normal messages
|
||||
prepare_msg_common(context, chat_id, msg, MessageState::OutPending).await?;
|
||||
} else {
|
||||
// update message state of separately prepared messages
|
||||
ensure!(
|
||||
chat_id.is_unset() || chat_id == msg.chat_id,
|
||||
"Inconsistent chat ID"
|
||||
);
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||
}
|
||||
let row_ids = create_send_msg_jobs(context, msg)
|
||||
.await
|
||||
.context("Failed to create send jobs")?;
|
||||
@@ -2942,8 +2982,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
recipients.push(from);
|
||||
}
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out
|
||||
if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden {
|
||||
// Webxdc integrations are messages, however, shipped with main app and must not be sent out
|
||||
if msg.param.get_int(Param::WebxdcIntegration).is_some() {
|
||||
recipients.clear();
|
||||
}
|
||||
|
||||
@@ -4120,6 +4160,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
bail!("cannot forward drafts.");
|
||||
}
|
||||
|
||||
let original_param = msg.param.clone();
|
||||
|
||||
// we tested a sort of broadcast
|
||||
// by not marking own forwarded messages as such,
|
||||
// however, this turned out to be to confusing and unclear.
|
||||
@@ -4142,13 +4184,33 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
msg.subject = "".to_string();
|
||||
|
||||
msg.state = MessageState::OutPending;
|
||||
let new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
let new_msg_id: MsgId;
|
||||
if msg.state == MessageState::OutPreparing {
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
msg.param = original_param;
|
||||
msg.id = src_msg_id;
|
||||
|
||||
if let Some(old_fwd) = msg.param.get(Param::PrepForwards) {
|
||||
let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32());
|
||||
msg.param.set(Param::PrepForwards, new_fwd);
|
||||
} else {
|
||||
msg.param
|
||||
.set(Param::PrepForwards, new_msg_id.to_u32().to_string());
|
||||
}
|
||||
|
||||
msg.update_param(context).await?;
|
||||
} else {
|
||||
msg.state = MessageState::OutPending;
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
}
|
||||
created_chats.push(chat_id);
|
||||
created_msgs.push(new_msg_id);
|
||||
@@ -4426,7 +4488,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
.await?;
|
||||
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
|
||||
|
||||
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
|
||||
// Insert labels for welcome messages to avoid them being readded on reconfiguration.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -4444,7 +4506,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
///
|
||||
/// For example, it can be a message showing that a member was added to a group.
|
||||
/// Doesn't fail if the chat doesn't exist.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn add_info_msg_with_cmd(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -4588,9 +4650,9 @@ impl Context {
|
||||
Contact::create_ex(self, Nosync, to, addr).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let addr = ContactAddress::new(addr).context("Invalid address")?;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(self, "", &addr, Origin::Hidden).await?;
|
||||
let contact_id = Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None)
|
||||
.await?
|
||||
.with_context(|| format!("No contact for addr '{addr}'"))?;
|
||||
match action {
|
||||
SyncAction::Block => {
|
||||
return contact::set_blocked(self, Nosync, contact_id, true).await
|
||||
@@ -4600,10 +4662,9 @@ impl Context {
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
// Use `Request` so that even if the program crashes, the user doesn't have to look
|
||||
// into the blocked contacts.
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
|
||||
ChatIdBlocked::lookup_by_contact(self, contact_id)
|
||||
.await?
|
||||
.with_context(|| format!("No chat for addr '{addr}'"))?
|
||||
.id
|
||||
}
|
||||
SyncId::Grpid(grpid) => {
|
||||
@@ -4640,7 +4701,7 @@ impl Context {
|
||||
|
||||
/// Emits the appropriate `MsgsChanged` event. Should be called if the number of unnoticed
|
||||
/// archived chats could decrease. In general we don't want to make an extra db query to know if
|
||||
/// a noticed chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
|
||||
/// a noticied chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
|
||||
/// is ok.
|
||||
pub(crate) fn on_archived_chats_maybe_noticed(&self) {
|
||||
self.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
|
||||
@@ -4655,7 +4716,7 @@ mod tests {
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{sync, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
use crate::test_utils::{sync, TestContext, TestContextManager};
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -4791,12 +4852,15 @@ mod tests {
|
||||
assert_eq!(test.text, "hello2".to_string());
|
||||
assert_eq!(test.state, MessageState::OutDraft);
|
||||
|
||||
let id_after_prepare = prepare_msg(&t, *chat_id, &mut msg).await?;
|
||||
assert_eq!(id_after_prepare, id_after_1st_set);
|
||||
let test = Message::load_from_db(&t, id_after_prepare).await?;
|
||||
assert_eq!(test.state, MessageState::OutPreparing);
|
||||
assert!(!test.hidden); // sent draft must no longer be hidden
|
||||
|
||||
let id_after_send = send_msg(&t, *chat_id, &mut msg).await?;
|
||||
assert_eq!(id_after_send, id_after_1st_set);
|
||||
|
||||
let test = Message::load_from_db(&t, id_after_send).await?;
|
||||
assert!(!test.hidden); // sent draft must no longer be hidden
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5196,8 +5260,6 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_modify_chat_disordered() -> Result<()> {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
// Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy
|
||||
// (sleep() is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then)
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -5548,6 +5610,7 @@ mod tests {
|
||||
|
||||
let mut msg = Message::new_text("message text".to_string());
|
||||
assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err());
|
||||
assert!(prepare_msg(&t, device_chat_id, &mut msg).await.is_err());
|
||||
|
||||
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
|
||||
assert!(forward_msgs(&t, &[msg_id], device_chat_id).await.is_err());
|
||||
@@ -7407,71 +7470,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_accept_before_first_msg() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.accept(alice0).await?;
|
||||
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(a0b_contact.origin, Origin::CreateChat);
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not);
|
||||
|
||||
sync(alice0, alice1).await;
|
||||
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(a1b_contact.origin, Origin::CreateChat);
|
||||
let a1b_chat = alice1.get_chat(&bob).await;
|
||||
assert_eq!(a1b_chat.blocked, Blocked::Not);
|
||||
let chats = Chatlist::try_load(alice1, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
|
||||
assert_eq!(rcvd_msg.chat_id, a1b_chat.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_block_before_first_msg() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.block(alice0).await?;
|
||||
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom);
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes);
|
||||
|
||||
sync(alice0, alice1).await;
|
||||
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(a1b_contact.origin, Origin::Hidden);
|
||||
assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id)
|
||||
.await?
|
||||
.is_none());
|
||||
|
||||
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
|
||||
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom);
|
||||
let a1b_chat = alice1.get_chat(&bob).await;
|
||||
assert_eq!(a1b_chat.blocked, Blocked::Yes);
|
||||
assert_eq!(rcvd_msg.chat_id, a1b_chat.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_adhoc_grp() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
@@ -7698,66 +7696,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_do_not_overwrite_draft() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let mut msg = Message::new_text("This is a draft message".to_string());
|
||||
let self_chat = alice.get_self_chat().await.id;
|
||||
self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
||||
let draft1 = self_chat.get_draft(&alice).await?.unwrap();
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
||||
let draft2 = self_chat.get_draft(&alice).await?.unwrap();
|
||||
assert_eq!(draft1.timestamp_sort, draft2.timestamp_sort);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test group consistency.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_member_bug() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
|
||||
|
||||
// Create a group.
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
|
||||
// Promote the group.
|
||||
let alice_sent_msg = alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
let bob_received_msg = bob.recv_msg(&alice_sent_msg).await;
|
||||
|
||||
let bob_chat_id = bob_received_msg.get_chat_id();
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
// Alice removes Fiona from the chat.
|
||||
remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
let _alice_sent_add_msg = alice.pop_sent_msg().await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
|
||||
// Bob sends a message
|
||||
// to Alice and Fiona because he still has not received
|
||||
// a message about Fiona being removed.
|
||||
let bob_sent_msg = bob.send_text(bob_chat_id, "Hi Alice!").await;
|
||||
|
||||
// Alice receives a message.
|
||||
// This should not add Fiona back.
|
||||
let _alice_received_msg = alice.recv_msg(&bob_sent_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,32 +394,25 @@ impl Chatlist {
|
||||
&chat_loaded
|
||||
};
|
||||
|
||||
let lastmsg = if let Some(lastmsg_id) = lastmsg_id {
|
||||
// Message may be deleted by the time we try to load it,
|
||||
// so use `load_from_db_optional` instead of `load_from_db`.
|
||||
Message::load_from_db_optional(context, lastmsg_id)
|
||||
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
|
||||
let lastmsg = Message::load_from_db(context, lastmsg_id)
|
||||
.await
|
||||
.context("Loading message failed")?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let lastcontact = if let Some(lastmsg) = &lastmsg {
|
||||
.context("loading message failed")?;
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
None
|
||||
(Some(lastmsg), None)
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
|
||||
.await
|
||||
.context("loading contact failed")?;
|
||||
Some(lastcontact)
|
||||
(Some(lastmsg), Some(lastcontact))
|
||||
}
|
||||
Chattype::Single => None,
|
||||
Chattype::Single => (Some(lastmsg), None),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if chat.id.is_archived_link() {
|
||||
@@ -768,25 +761,6 @@ mod tests {
|
||||
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
|
||||
}
|
||||
|
||||
/// Tests that summary does not fail to load
|
||||
/// if the draft was deleted after loading the chatlist.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_summary_deleted_draft() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut msg = Message::new_text("Foobar".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
chat_id.set_draft(&t, None).await.unwrap();
|
||||
|
||||
let summary_res = chats.get_summary(&t, 0, None).await;
|
||||
assert!(summary_res.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_broken() {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
@@ -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,
|
||||
@@ -441,12 +441,6 @@ pub enum Config {
|
||||
/// Enable webxdc realtime features.
|
||||
#[strum(props(default = "1"))]
|
||||
WebxdcRealtimeEnabled,
|
||||
|
||||
/// Last device token stored on the chatmail server.
|
||||
///
|
||||
/// If it has not changed, we do not store
|
||||
/// the device token again.
|
||||
DeviceToken,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -797,12 +791,6 @@ impl Context {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
key,
|
||||
Config::Displayname | Config::Selfavatar | Config::PrivateTag
|
||||
) {
|
||||
self.emit_event(EventType::AccountsItemChanged);
|
||||
}
|
||||
if key.is_synced() {
|
||||
self.emit_event(EventType::ConfigSynced { key });
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ use crate::message::Message;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::{chat, e2ee, provider};
|
||||
use crate::{stock_str, EventType};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
|
||||
macro_rules! progress {
|
||||
@@ -486,7 +486,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
update_device_chats_handle.await??;
|
||||
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
ctx.emit_event(EventType::AccountsItemChanged);
|
||||
|
||||
Ok(configured_param)
|
||||
}
|
||||
@@ -612,6 +611,8 @@ pub enum Error {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::login_param::EnteredServerLoginParam;
|
||||
|
||||
@@ -67,15 +67,19 @@ fn parse_server<B: BufRead>(
|
||||
|
||||
let typ = server_event
|
||||
.attributes()
|
||||
.find_map(|attr| {
|
||||
attr.ok().filter(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("type")
|
||||
})
|
||||
.find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "type"
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.map(|typ| {
|
||||
typ.decode_and_unescape_value(reader.decoder())
|
||||
typ.unwrap()
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
})
|
||||
@@ -268,6 +272,8 @@ pub(crate) async fn moz_autoconfigure(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -215,6 +215,8 @@ pub(crate) async fn outlk_autodiscover(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -144,7 +144,7 @@ impl ContactId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns contact address.
|
||||
/// Returns contact adress.
|
||||
pub async fn addr(&self, context: &Context) -> Result<String> {
|
||||
let addr = context
|
||||
.sql
|
||||
@@ -805,6 +805,7 @@ impl Contact {
|
||||
}
|
||||
|
||||
let mut name = sanitize_name(name);
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if origin <= Origin::OutgoingTo {
|
||||
// The user may accidentally have written to a "noreply" address with another MUA:
|
||||
if addr.contains("noreply")
|
||||
@@ -1251,15 +1252,15 @@ impl Contact {
|
||||
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
.await?
|
||||
.dc_fingerprint()
|
||||
.fingerprint()
|
||||
.to_string();
|
||||
let fingerprint_other_verified = peerstate
|
||||
.peek_key(true)
|
||||
.map(|k| k.dc_fingerprint().to_string())
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_other_unverified = peerstate
|
||||
.peek_key(false)
|
||||
.map(|k| k.dc_fingerprint().to_string())
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
if addr < peerstate.addr {
|
||||
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
|
||||
@@ -1983,7 +1984,7 @@ mod tests {
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
use crate::test_utils::{self, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_contact_id_values() {
|
||||
@@ -2914,8 +2915,6 @@ Hi."#;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently() -> Result<()> {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
@@ -2931,7 +2930,18 @@ Hi."#;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
|
||||
assert!(contact.was_seen_recently());
|
||||
let green = nu_ansi_term::Color::Green.normal();
|
||||
assert!(
|
||||
contact.was_seen_recently(),
|
||||
"{}",
|
||||
green.paint(
|
||||
"\nNOTE: This test failure is probably a false-positive, caused by tests running in parallel.
|
||||
The issue is that `SystemTime::shift()` (a utility function for tests) changes the time for all threads doing tests, and not only for the running test.
|
||||
Until the false-positive is fixed:
|
||||
- Use `cargo test -- --test-threads 1` instead of `cargo test`
|
||||
- Or use `cargo nextest run` (install with `cargo install cargo-nextest --locked`)\n"
|
||||
)
|
||||
);
|
||||
|
||||
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
|
||||
assert!(!self_contact.was_seen_recently());
|
||||
@@ -3234,20 +3244,4 @@ Hi."#;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_is_verified() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
|
||||
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
|
||||
assert_eq!(contact.is_verified(&alice).await?, true);
|
||||
assert!(contact.is_profile_verified(&alice).await?);
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
|
||||
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
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};
|
||||
@@ -276,7 +275,7 @@ pub struct InnerContext {
|
||||
/// The text of the last error logged and emitted as an event.
|
||||
/// If the ui wants to display an error after a failure,
|
||||
/// `last_error` should be used to avoid races with the event thread.
|
||||
pub(crate) last_error: parking_lot::RwLock<String>,
|
||||
pub(crate) last_error: std::sync::RwLock<String>,
|
||||
|
||||
/// If debug logging is enabled, this contains all necessary information
|
||||
///
|
||||
@@ -292,7 +291,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.
|
||||
@@ -446,11 +445,11 @@ impl Context {
|
||||
metadata: RwLock::new(None),
|
||||
creation_time: tools::Time::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
last_error: parking_lot::RwLock::new("".to_string()),
|
||||
last_error: std::sync::RwLock::new("".to_string()),
|
||||
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 {
|
||||
@@ -472,33 +471,12 @@ impl Context {
|
||||
// Allow at least 1 message every second + a burst of 3.
|
||||
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
|
||||
}
|
||||
|
||||
// The next line is mainly for iOS:
|
||||
// iOS starts a separate process for receiving notifications and if the user concurrently
|
||||
// starts the app, the UI process opens the database but waits with calling start_io()
|
||||
// until the notifications process finishes.
|
||||
// Now, some configs may have changed, so, we need to invalidate the cache.
|
||||
self.sql.config_cache.write().await.clear();
|
||||
|
||||
self.scheduler.start(self.clone()).await;
|
||||
}
|
||||
|
||||
/// 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 +487,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 +531,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.
|
||||
@@ -787,7 +781,7 @@ impl Context {
|
||||
.count("SELECT COUNT(*) FROM acpeerstates;", ())
|
||||
.await?;
|
||||
let fingerprint_str = match load_self_public_key(self).await {
|
||||
Ok(key) => key.dc_fingerprint().hex(),
|
||||
Ok(key) => key.fingerprint().hex(),
|
||||
Err(err) => format!("<key failure: {err}>"),
|
||||
};
|
||||
|
||||
@@ -1183,7 +1177,7 @@ impl Context {
|
||||
EncryptPreference::Mutual,
|
||||
&public_key,
|
||||
);
|
||||
let fingerprint = public_key.dc_fingerprint();
|
||||
let fingerprint = public_key.fingerprint();
|
||||
peerstate.set_verified(public_key, fingerprint, "".to_string())?;
|
||||
peerstate.save_to_db(&self.sql).await?;
|
||||
chat_id
|
||||
@@ -1755,7 +1749,6 @@ mod tests {
|
||||
"socks5_password",
|
||||
"key_id",
|
||||
"webxdc_integration",
|
||||
"device_token",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
@@ -2070,41 +2063,4 @@ mod tests {
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,9 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
|
||||
"time": time,
|
||||
}),
|
||||
info: None,
|
||||
href: None,
|
||||
summary: None,
|
||||
document: None,
|
||||
uid: None,
|
||||
notify: None,
|
||||
},
|
||||
time,
|
||||
)
|
||||
|
||||
@@ -182,7 +182,7 @@ pub(crate) async fn get_autocrypt_peerstate(
|
||||
// if the fingerprint is verified.
|
||||
peerstate = Peerstate::from_verified_fingerprint_or_addr(
|
||||
context,
|
||||
&header.public_key.dc_fingerprint(),
|
||||
&header.public_key.fingerprint(),
|
||||
from,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -440,7 +440,7 @@ mod tests {
|
||||
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
|
||||
|
||||
alice
|
||||
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#)
|
||||
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#, "d")
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -303,12 +303,12 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
last_seen_autocrypt: 14,
|
||||
prefer_encrypt,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
public_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
gossip_key: Some(pub_key.clone()),
|
||||
gossip_timestamp: 15,
|
||||
gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
gossip_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
verified_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
|
||||
@@ -930,6 +930,7 @@ mod tests {
|
||||
|
||||
// Alice sends a text message.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -956,12 +957,14 @@ mod tests {
|
||||
|
||||
// Alice sends message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Alice sends second message to Bob, with no timer
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -107,21 +107,6 @@ pub enum EventType {
|
||||
reaction: Reaction,
|
||||
},
|
||||
|
||||
/// A webxdc wants an info message or a changed summary to be notified.
|
||||
IncomingWebxdcNotify {
|
||||
/// ID of the contact sending.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// ID of the added info message or webxdc instance in case of summary change.
|
||||
msg_id: MsgId,
|
||||
|
||||
/// Text to notify.
|
||||
text: String,
|
||||
|
||||
/// Link assigned to this notification, if any.
|
||||
href: Option<String>,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -347,20 +332,6 @@ pub enum EventType {
|
||||
chat_id: Option<ChatId>,
|
||||
},
|
||||
|
||||
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
|
||||
///
|
||||
/// This event is only emitted by the account manager
|
||||
AccountsChanged,
|
||||
|
||||
/// Inform that an account property that might be shown in the account list changed, namely:
|
||||
/// - is_configured (see [crate::context::Context::is_configured])
|
||||
/// - displayname
|
||||
/// - selfavatar
|
||||
/// - private_tag
|
||||
///
|
||||
/// This event is emitted from the account whose property changed.
|
||||
AccountsItemChanged,
|
||||
|
||||
/// Event for using in tests, e.g. as a fence between normally generated events.
|
||||
#[cfg(test)]
|
||||
Test,
|
||||
|
||||
78
src/html.rs
78
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);
|
||||
@@ -302,7 +274,7 @@ mod tests {
|
||||
async fn test_htmlparse_plain_unspecified() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -320,7 +292,7 @@ This message does not have Content-Type nor Subject.<br/>
|
||||
async fn test_htmlparse_plain_iso88591() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -338,7 +310,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
|
||||
async fn test_htmlparse_plain_flowed() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert!(parser.plain.unwrap().flowed);
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
@@ -360,7 +332,7 @@ and will be wrapped as usual.<br/>
|
||||
async fn test_htmlparse_alt_plain() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -381,7 +353,7 @@ test some special html-characters as < > 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`;
|
||||
@@ -399,7 +371,7 @@ 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>
|
||||
@@ -414,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>
|
||||
@@ -439,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"));
|
||||
|
||||
79
src/imap.rs
79
src/imap.rs
@@ -41,7 +41,6 @@ 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,
|
||||
};
|
||||
@@ -408,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);
|
||||
}
|
||||
@@ -1301,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,
|
||||
@@ -1561,54 +1560,16 @@ impl Session {
|
||||
};
|
||||
|
||||
if self.can_metadata() && self.can_push() {
|
||||
let device_token_changed = context
|
||||
.get_config(Config::DeviceToken)
|
||||
let folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.map_or(true, |config_token| device_token != config_token);
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
if device_token_changed {
|
||||
let folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
let encrypted_device_token = encrypt_device_token(&device_token)
|
||||
.context("Failed to encrypt device token")?;
|
||||
|
||||
// We expect that the server supporting `XDELTAPUSH` capability
|
||||
// has non-synchronizing literals support as well:
|
||||
// <https://www.rfc-editor.org/rfc/rfc7888>.
|
||||
let encrypted_device_token_len = encrypted_device_token.len();
|
||||
|
||||
if encrypted_device_token_len <= 4096 {
|
||||
self.run_command_and_check_ok(&format_setmetadata(
|
||||
&folder,
|
||||
&encrypted_device_token,
|
||||
))
|
||||
.await
|
||||
.context("SETMETADATA command failed")?;
|
||||
|
||||
// Store device token saved on the server
|
||||
// to prevent storing duplicate tokens.
|
||||
// The server cannot deduplicate on its own
|
||||
// because encryption gives a different
|
||||
// result each time.
|
||||
context
|
||||
.set_config_internal(Config::DeviceToken, Some(&device_token))
|
||||
.await?;
|
||||
} else {
|
||||
// If Apple or Google (FCM) gives us a very large token,
|
||||
// do not even try to give it to IMAP servers.
|
||||
//
|
||||
// Limit of 4096 is arbitrarily selected
|
||||
// to be the same as required by LITERAL- IMAP extension.
|
||||
//
|
||||
// Dovecot supports LITERAL+ and non-synchronizing literals
|
||||
// of any length, but there is no reason for tokens
|
||||
// to be that large even after OpenPGP encryption.
|
||||
warn!(context, "Device token is too long for LITERAL-, ignoring.");
|
||||
}
|
||||
}
|
||||
self.run_command_and_check_ok(format!(
|
||||
"SETMETADATA \"{folder}\" (/private/devicetoken \"{device_token}\")"
|
||||
))
|
||||
.await
|
||||
.context("SETMETADATA command failed")?;
|
||||
context.push_subscribed.store(true, Ordering::Relaxed);
|
||||
} else if !context.push_subscriber.heartbeat_subscribed().await {
|
||||
let context = context.clone();
|
||||
@@ -1620,13 +1581,6 @@ 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
|
||||
@@ -2689,6 +2643,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn check_target_folder_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
@@ -2909,16 +2864,4 @@ mod tests {
|
||||
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_setmetadata_device_token() {
|
||||
assert_eq!(
|
||||
format_setmetadata("INBOX", "foobarbaz"),
|
||||
"SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)"
|
||||
);
|
||||
assert_eq!(
|
||||
format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"),
|
||||
"SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
|
||||
use ::pgp::types::PublicKeyTrait;
|
||||
use ::pgp::types::KeyTrait;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use futures::TryStreamExt;
|
||||
use futures_lite::FutureExt;
|
||||
@@ -426,7 +426,6 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
if res.is_ok() {
|
||||
context.emit_event(EventType::ImexProgress(999));
|
||||
res = context.sql.run_migrations(context).await;
|
||||
context.emit_event(EventType::AccountsItemChanged);
|
||||
}
|
||||
if res.is_ok() {
|
||||
delete_and_reset_all_device_msgs(context)
|
||||
@@ -751,7 +750,7 @@ where
|
||||
true => "private",
|
||||
};
|
||||
let id = id.map_or("default".into(), |i| i.to_string());
|
||||
let fp = key.dc_fingerprint().hex();
|
||||
let fp = DcKey::fingerprint(key).hex();
|
||||
format!("{kind}-key-{addr}-{id}-{fp}.asc")
|
||||
};
|
||||
let path = dir.join(&file_name);
|
||||
@@ -879,7 +878,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.strip_suffix(".asc")
|
||||
.unwrap();
|
||||
assert_eq!(fingerprint, key.dc_fingerprint().hex());
|
||||
assert_eq!(fingerprint, DcKey::fingerprint(&key).hex());
|
||||
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
|
||||
let filename = format!("{blobdir}/{filename}");
|
||||
let bytes = tokio::fs::read(&filename).await.unwrap();
|
||||
|
||||
82
src/key.rs
82
src/key.rs
@@ -11,8 +11,7 @@ use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{PublicKeyTrait, SecretKeyTrait};
|
||||
use rand::thread_rng;
|
||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -27,42 +26,10 @@ use crate::tools::{self, time_elapsed};
|
||||
/// This trait is implemented for rPGP's [SignedPublicKey] and
|
||||
/// [SignedSecretKey] types and makes working with them a little
|
||||
/// easier in the deltachat world.
|
||||
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
|
||||
pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + 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.
|
||||
@@ -126,8 +93,8 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
|
||||
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
|
||||
|
||||
/// The fingerprint for the key.
|
||||
fn dc_fingerprint(&self) -> Fingerprint {
|
||||
PublicKeyTrait::fingerprint(self).into()
|
||||
fn fingerprint(&self) -> Fingerprint {
|
||||
Fingerprint::new(KeyTrait::fingerprint(self))
|
||||
}
|
||||
|
||||
fn is_private() -> bool;
|
||||
@@ -266,8 +233,7 @@ impl DcSecretKey for SignedSecretKey {
|
||||
fn split_public_key(&self) -> Result<SignedPublicKey> {
|
||||
self.verify()?;
|
||||
let unsigned_pubkey = SecretKeyTrait::public_key(self);
|
||||
let mut rng = thread_rng();
|
||||
let signed_pubkey = unsigned_pubkey.sign(&mut rng, self, || "".into())?;
|
||||
let signed_pubkey = unsigned_pubkey.sign(self, || "".into())?;
|
||||
Ok(signed_pubkey)
|
||||
}
|
||||
}
|
||||
@@ -429,12 +395,6 @@ impl Fingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pgp::types::Fingerprint> for Fingerprint {
|
||||
fn from(fingerprint: pgp::types::Fingerprint) -> Fingerprint {
|
||||
Self::new(fingerprint.as_bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Fingerprint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Fingerprint")
|
||||
@@ -597,36 +557,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();
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
|
||||
@@ -87,6 +87,17 @@ pub struct Kml {
|
||||
pub curr: Location,
|
||||
}
|
||||
|
||||
/// Location streaming status for one chat.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum LocationSendingStatus {
|
||||
/// Location streaming is disabled.
|
||||
Disabled = 0,
|
||||
/// Location streaming is enabled.
|
||||
Enabled = 1,
|
||||
/// Location streaming is enabled but (currently) not possible.
|
||||
Failure = 2,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
enum KmlTag {
|
||||
#[default]
|
||||
@@ -244,14 +255,18 @@ impl Kml {
|
||||
self.tag = KmlTag::PlacemarkPoint;
|
||||
} else if tag == "coordinates" && self.tag == KmlTag::PlacemarkPoint {
|
||||
self.tag = KmlTag::PlacemarkPointCoordinates;
|
||||
if let Some(acc) = event.attributes().find_map(|attr| {
|
||||
attr.ok().filter(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("accuracy")
|
||||
})
|
||||
if let Some(acc) = event.attributes().find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "accuracy"
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}) {
|
||||
let v = acc
|
||||
.unwrap()
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -270,7 +285,8 @@ pub async fn send_locations_to_chat(
|
||||
ensure!(seconds >= 0);
|
||||
ensure!(!chat_id.is_special());
|
||||
let now = time();
|
||||
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?;
|
||||
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?
|
||||
== LocationSendingStatus::Enabled;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -310,25 +326,43 @@ pub async fn send_locations_to_chat(
|
||||
pub async fn is_sending_locations_to_chat(
|
||||
context: &Context,
|
||||
chat_id: Option<ChatId>,
|
||||
) -> Result<bool> {
|
||||
) -> Result<LocationSendingStatus> {
|
||||
let exists = match chat_id {
|
||||
Some(chat_id) => {
|
||||
context
|
||||
let enabled = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
|
||||
(chat_id, time()),
|
||||
)
|
||||
.await?;
|
||||
let functional: i32 = context
|
||||
.sql
|
||||
.query_get_value("SELECT locations_send_begin FROM chats WHERE id=?;", ())
|
||||
.await?
|
||||
.ok_or(anyhow::anyhow!("not able to select"))?;
|
||||
|
||||
if enabled && functional > 0 {
|
||||
LocationSendingStatus::Enabled
|
||||
} else if enabled && functional == 0 {
|
||||
LocationSendingStatus::Failure
|
||||
} else {
|
||||
LocationSendingStatus::Disabled
|
||||
}
|
||||
}
|
||||
None => {
|
||||
context
|
||||
if context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
|
||||
(time(),),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
LocationSendingStatus::Enabled
|
||||
} else {
|
||||
LocationSendingStatus::Disabled
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(exists)
|
||||
@@ -876,6 +910,8 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::message::MessageState;
|
||||
|
||||
@@ -50,13 +50,13 @@ impl Context {
|
||||
/// Set last error string.
|
||||
/// Implemented as blocking as used from macros in different, not always async blocks.
|
||||
pub fn set_last_error(&self, error: &str) {
|
||||
let mut last_error = self.last_error.write();
|
||||
let mut last_error = self.last_error.write().unwrap();
|
||||
*last_error = error.to_string();
|
||||
}
|
||||
|
||||
/// Get last error string.
|
||||
pub fn get_last_error(&self) -> String {
|
||||
let last_error = &*self.last_error.read();
|
||||
let last_error = &*self.last_error.read().unwrap();
|
||||
last_error.clone()
|
||||
}
|
||||
}
|
||||
|
||||
169
src/message.rs
169
src/message.rs
@@ -348,7 +348,7 @@ impl MsgId {
|
||||
let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
|
||||
for server_url in server_urls {
|
||||
// Format as RFC 5092 relative IMAP URL.
|
||||
ret += &format!("\nServer-URL: {server_url}");
|
||||
ret += &format!("\n{server_url}");
|
||||
}
|
||||
}
|
||||
let hop_info = self.hop_info(context).await?;
|
||||
@@ -724,7 +724,7 @@ impl Message {
|
||||
/// `contact_id` set to [`ContactId::SELF`].
|
||||
///
|
||||
/// `latitude` is the North-south position of the location.
|
||||
/// `longitude` is the East-west position of the location.
|
||||
/// `longitutde` is the East-west position of the location.
|
||||
///
|
||||
/// [`location::set()`]: crate::location::set
|
||||
/// [`send_locations_to_chat()`]: crate::location::send_locations_to_chat
|
||||
@@ -953,6 +953,18 @@ impl Message {
|
||||
cmd != SystemMessage::Unknown
|
||||
}
|
||||
|
||||
/// Whether the message is still being created.
|
||||
///
|
||||
/// Messages with attachments might be created before the
|
||||
/// attachment is ready. In this case some more restrictions on
|
||||
/// the attachment apply, e.g. if the file to be attached is still
|
||||
/// being written to or otherwise will still change it can not be
|
||||
/// copied to the blobdir. Thus those attachments need to be
|
||||
/// created immediately in the blobdir with a valid filename.
|
||||
pub fn is_increation(&self) -> bool {
|
||||
self.viewtype.has_file() && self.state == MessageState::OutPreparing
|
||||
}
|
||||
|
||||
/// Returns true if the message is an Autocrypt Setup Message.
|
||||
pub fn is_setupmessage(&self) -> bool {
|
||||
if self.viewtype != Viewtype::File {
|
||||
@@ -1681,7 +1693,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
m.id AS id,
|
||||
m.chat_id AS chat_id,
|
||||
m.state AS state,
|
||||
m.download_state as download_state,
|
||||
m.ephemeral_timer AS ephemeral_timer,
|
||||
m.param AS param,
|
||||
m.from_id AS from_id,
|
||||
@@ -1697,7 +1708,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let download_state: DownloadState = row.get("download_state")?;
|
||||
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
@@ -1709,7 +1719,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
download_state,
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
@@ -1739,7 +1748,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
id,
|
||||
curr_chat_id,
|
||||
curr_state,
|
||||
curr_download_state,
|
||||
curr_param,
|
||||
curr_from_id,
|
||||
curr_rfc724_mid,
|
||||
@@ -1749,14 +1757,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
_curr_ephemeral_timer,
|
||||
) in msgs
|
||||
{
|
||||
if curr_download_state != DownloadState::Done {
|
||||
if curr_state == MessageState::InFresh {
|
||||
// Don't mark partially downloaded messages as seen or send a read receipt since
|
||||
// they are not really seen by the user.
|
||||
update_msg_state(context, id, MessageState::InNoticed).await?;
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
} else if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||||
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||||
update_msg_state(context, id, MessageState::InSeen).await?;
|
||||
info!(context, "Seen message {}.", id);
|
||||
|
||||
@@ -2194,6 +2195,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prepare_message_and_send() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = d.create_chat_with_contact("", "dest@example.com").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
let msg_id = chat::prepare_msg(ctx, chat.id, &mut msg).await.unwrap();
|
||||
|
||||
let _msg2 = Message::load_from_db(ctx, msg_id).await.unwrap();
|
||||
assert_eq!(_msg2.get_filemime(), None);
|
||||
}
|
||||
|
||||
/// Tests that message can be prepared even if account has no configured address.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prepare_not_configured() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
let chat = d.create_chat_with_contact("", "dest@example.com").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webrtc_instance() {
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
|
||||
@@ -2313,9 +2346,9 @@ mod tests {
|
||||
|
||||
let mut msg = Message::new_text("Quoted message".to_string());
|
||||
|
||||
// Send message, so it gets a Message-Id.
|
||||
// Prepare message for sending, so it gets a Message-Id.
|
||||
assert!(msg.rfc724_mid.is_empty());
|
||||
let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap();
|
||||
let msg_id = chat::prepare_msg(ctx, chat.id, &mut msg).await.unwrap();
|
||||
let msg = Message::load_from_db(ctx, msg_id).await.unwrap();
|
||||
assert!(!msg.rfc724_mid.is_empty());
|
||||
|
||||
@@ -2550,110 +2583,6 @@ mod tests {
|
||||
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)
|
||||
.await?;
|
||||
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)
|
||||
.await?;
|
||||
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;
|
||||
|
||||
@@ -6,6 +6,7 @@ use anyhow::{bail, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use chrono::TimeZone;
|
||||
use email::Mailbox;
|
||||
use format_flowed::{format_flowed, format_flowed_quote};
|
||||
use lettre_email::{Address, Header, MimeMultipartType, PartBuilder};
|
||||
use tokio::fs;
|
||||
|
||||
@@ -19,7 +20,7 @@ use crate::e2ee::EncryptHelper;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::html::new_html_mimepart;
|
||||
use crate::location;
|
||||
use crate::location::{self, LocationSendingStatus};
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
@@ -356,7 +357,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)
|
||||
@@ -1047,6 +1048,7 @@ impl MimeFactory {
|
||||
part.body(text)
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn render_message(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1298,18 +1300,9 @@ impl MimeFactory {
|
||||
|
||||
let final_text = placeholdertext.as_deref().unwrap_or(&msg.text);
|
||||
|
||||
let mut quoted_text = None;
|
||||
if let Some(msg_quoted_text) = msg.quoted_text() {
|
||||
let mut some_quoted_text = String::new();
|
||||
for quoted_line in msg_quoted_text.split('\n') {
|
||||
some_quoted_text += "> ";
|
||||
some_quoted_text += quoted_line;
|
||||
some_quoted_text += "\r\n";
|
||||
}
|
||||
some_quoted_text += "\r\n";
|
||||
quoted_text = Some(some_quoted_text)
|
||||
}
|
||||
|
||||
let mut quoted_text = msg
|
||||
.quoted_text()
|
||||
.map(|quote| format_flowed_quote("e) + "\r\n\r\n");
|
||||
if !is_encrypted && msg.param.get_bool(Param::ProtectQuote).unwrap_or_default() {
|
||||
// Message is not encrypted but quotes encrypted message.
|
||||
quoted_text = Some("> ...\r\n\r\n".to_string());
|
||||
@@ -1319,6 +1312,7 @@ impl MimeFactory {
|
||||
// Delta Chat.
|
||||
quoted_text = Some("\r\n".to_string());
|
||||
}
|
||||
let flowed_text = format_flowed(final_text);
|
||||
|
||||
let is_reaction = msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
|
||||
|
||||
@@ -1328,7 +1322,7 @@ impl MimeFactory {
|
||||
"{}{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
quoted_text.unwrap_or_default(),
|
||||
escape_message_footer_marks(final_text),
|
||||
escape_message_footer_marks(&flowed_text),
|
||||
if !final_text.is_empty() && !footer.is_empty() {
|
||||
"\r\n\r\n"
|
||||
} else {
|
||||
@@ -1338,8 +1332,10 @@ impl MimeFactory {
|
||||
footer
|
||||
);
|
||||
|
||||
let mut main_part =
|
||||
PartBuilder::new().header(("Content-Type", "text/plain; charset=utf-8"));
|
||||
let mut main_part = PartBuilder::new().header((
|
||||
"Content-Type",
|
||||
"text/plain; charset=utf-8; format=flowed; delsp=no",
|
||||
));
|
||||
main_part = self.add_message_text(main_part, message_text);
|
||||
|
||||
if is_reaction {
|
||||
@@ -1376,7 +1372,9 @@ impl MimeFactory {
|
||||
parts.push(msg_kml_part);
|
||||
}
|
||||
|
||||
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await? {
|
||||
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await?
|
||||
!= LocationSendingStatus::Disabled
|
||||
{
|
||||
if let Some(part) = self.get_location_kml_part(context).await? {
|
||||
parts.push(part);
|
||||
}
|
||||
@@ -1515,7 +1513,7 @@ async fn build_body_file(
|
||||
) -> 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");
|
||||
@@ -1904,7 +1902,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\
|
||||
@@ -1930,9 +1928,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());
|
||||
@@ -2079,7 +2074,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();
|
||||
|
||||
@@ -2136,7 +2131,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();
|
||||
@@ -2166,9 +2161,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()
|
||||
}
|
||||
@@ -2189,6 +2181,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
|
||||
}
|
||||
@@ -2199,7 +2194,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\
|
||||
@@ -2212,7 +2207,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();
|
||||
|
||||
@@ -2282,6 +2276,7 @@ mod tests {
|
||||
let msg = message.as_string();
|
||||
|
||||
let header_end = msg.find("Hi").unwrap();
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
let headers = msg[0..header_end].trim();
|
||||
|
||||
assert!(!headers.lines().any(|l| l.trim().is_empty()));
|
||||
|
||||
@@ -17,10 +17,10 @@ use rand::distributions::{Alphanumeric, DistString};
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::authres::handle_authres;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{add_info_msg, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::constants::{self, Chattype};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{
|
||||
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
|
||||
@@ -36,7 +36,8 @@ use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::{
|
||||
get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text, validate_id,
|
||||
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text,
|
||||
validate_id,
|
||||
};
|
||||
use crate::{chatlist_events, location, stock_str, tools};
|
||||
|
||||
@@ -105,12 +106,14 @@ pub(crate) struct MimeMessage {
|
||||
/// received.
|
||||
pub(crate) footer: Option<String>,
|
||||
|
||||
/// If set, this is a modified MIME message; clients should offer a way to view the original
|
||||
/// MIME message in this case.
|
||||
// if this flag is set, the parts/text/etc. are just close to the original mime-message;
|
||||
// clients should offer a way to view the original message in this case
|
||||
pub is_mime_modified: bool,
|
||||
|
||||
/// Decrypted, raw MIME structure. Nonempty iff `is_mime_modified` and the message was actually
|
||||
/// encrypted.
|
||||
/// The decrypted, raw mime structure.
|
||||
///
|
||||
/// This is non-empty iff `is_mime_modified` and the message was actually encrypted. It is used
|
||||
/// for e.g. late-parsing HTML.
|
||||
pub decoded_data: Vec<u8>,
|
||||
|
||||
/// Hop info for debugging.
|
||||
@@ -662,13 +665,11 @@ impl MimeMessage {
|
||||
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
|
||||
/// containing a description. If such a message is detected, text from the first part can be
|
||||
/// moved to the second part, and the first part dropped.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn squash_attachment_parts(&mut self) {
|
||||
if self.parts.len() == 2
|
||||
&& self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
|
||||
&& self
|
||||
.parts
|
||||
.get(1)
|
||||
.is_some_and(|filepart| match filepart.typ {
|
||||
if let [textpart, filepart] = &self.parts[..] {
|
||||
let need_drop = textpart.typ == Viewtype::Text
|
||||
&& match filepart.typ {
|
||||
Viewtype::Image
|
||||
| Viewtype::Gif
|
||||
| Viewtype::Sticker
|
||||
@@ -679,24 +680,24 @@ impl MimeMessage {
|
||||
| Viewtype::File
|
||||
| Viewtype::Webxdc => true,
|
||||
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
|
||||
})
|
||||
{
|
||||
let mut parts = std::mem::take(&mut self.parts);
|
||||
let Some(mut filepart) = parts.pop() else {
|
||||
// Should never happen.
|
||||
return;
|
||||
};
|
||||
let Some(textpart) = parts.pop() else {
|
||||
// Should never happen.
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
filepart.msg.clone_from(&textpart.msg);
|
||||
if let Some(quote) = textpart.param.get(Param::Quote) {
|
||||
filepart.param.set(Param::Quote, quote);
|
||||
if need_drop {
|
||||
let mut filepart = self.parts.swap_remove(1);
|
||||
|
||||
// insert new one
|
||||
filepart.msg.clone_from(&self.parts[0].msg);
|
||||
if let Some(quote) = self.parts[0].param.get(Param::Quote) {
|
||||
filepart.param.set(Param::Quote, quote);
|
||||
}
|
||||
|
||||
// forget the one we use now
|
||||
self.parts[0].msg = "".to_string();
|
||||
|
||||
// swap new with old
|
||||
self.parts.push(filepart); // push to the end
|
||||
let _ = self.parts.swap_remove(0); // drops first element, replacing it with the last one in O(1)
|
||||
}
|
||||
|
||||
self.parts = vec![filepart];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1279,7 +1280,7 @@ impl MimeMessage {
|
||||
Ok(self.parts.len() > old_part_count)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn do_add_single_file_part(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1662,8 +1663,17 @@ impl MimeMessage {
|
||||
.get_header_value(HeaderDef::MessageId)
|
||||
.and_then(|v| parse_message_id(&v).ok())
|
||||
{
|
||||
let mut to_list =
|
||||
get_all_addresses_from_header(&report.headers, "x-failed-recipients");
|
||||
let to = if to_list.len() == 1 {
|
||||
Some(to_list.pop().unwrap())
|
||||
} else {
|
||||
None // We do not know which recipient failed
|
||||
};
|
||||
|
||||
return Ok(Some(DeliveryReport {
|
||||
rfc724_mid: original_message_id,
|
||||
failed_recipient: to.map(|s| s.addr),
|
||||
failure,
|
||||
}));
|
||||
}
|
||||
@@ -1741,6 +1751,7 @@ impl MimeMessage {
|
||||
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
|
||||
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
|
||||
/// Also you should add a test in receive_imf.rs (there already are lots of test_parse_ndn_* tests).
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn heuristically_parse_ndn(&mut self, context: &Context) {
|
||||
let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
|
||||
let from = from.to_ascii_lowercase();
|
||||
@@ -1761,6 +1772,7 @@ impl MimeMessage {
|
||||
{
|
||||
self.delivery_report = Some(DeliveryReport {
|
||||
rfc724_mid: original_message_id,
|
||||
failed_recipient: None,
|
||||
failure: true,
|
||||
})
|
||||
}
|
||||
@@ -1898,20 +1910,22 @@ pub(crate) struct Report {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DeliveryReport {
|
||||
pub rfc724_mid: String,
|
||||
pub failed_recipient: Option<String>,
|
||||
pub failure: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
|
||||
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
|
||||
let mut msgids = Vec::new();
|
||||
for id in ids.split_whitespace() {
|
||||
let mut id = id.to_string();
|
||||
if let Some(id_without_prefix) = id.strip_prefix('<') {
|
||||
id = id_without_prefix.to_string();
|
||||
};
|
||||
if let Some(id_without_suffix) = id.strip_suffix('>') {
|
||||
id = id_without_suffix.to_string();
|
||||
};
|
||||
if id.starts_with('<') {
|
||||
id = id[1..].to_string();
|
||||
}
|
||||
if id.ends_with('>') {
|
||||
id = id[..id.len() - 1].to_string();
|
||||
}
|
||||
if !id.is_empty() {
|
||||
msgids.push(id);
|
||||
}
|
||||
@@ -2263,12 +2277,20 @@ async fn handle_ndn(
|
||||
let msgs: Vec<_> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM msgs
|
||||
WHERE rfc724_mid=? AND from_id=1",
|
||||
concat!(
|
||||
"SELECT",
|
||||
" m.id AS msg_id,",
|
||||
" c.id AS chat_id,",
|
||||
" c.type AS type",
|
||||
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
|
||||
" WHERE rfc724_mid=? AND from_id=1",
|
||||
),
|
||||
(&failed.rfc724_mid,),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
let msg_id: MsgId = row.get("msg_id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let chat_type: Chattype = row.get("type")?;
|
||||
Ok((msg_id, chat_id, chat_type))
|
||||
},
|
||||
|rows| Ok(rows.collect::<Vec<_>>()),
|
||||
)
|
||||
@@ -2276,13 +2298,16 @@ async fn handle_ndn(
|
||||
|
||||
let error = if let Some(error) = error {
|
||||
error
|
||||
} else if let Some(failed_recipient) = &failed.failed_recipient {
|
||||
format!("Delivery to {failed_recipient} failed.").clone()
|
||||
} else {
|
||||
"Delivery to at least one recipient failed.".to_string()
|
||||
};
|
||||
let err_msg = &error;
|
||||
|
||||
let mut first = true;
|
||||
for msg in msgs {
|
||||
let msg_id = msg?;
|
||||
let (msg_id, chat_id, chat_type) = msg?;
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
let aggregated_error = message
|
||||
.error
|
||||
@@ -2294,13 +2319,51 @@ async fn handle_ndn(
|
||||
aggregated_error.as_ref().unwrap_or(err_msg),
|
||||
)
|
||||
.await?;
|
||||
if first {
|
||||
// Add only one info msg for all failed messages
|
||||
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ndn_maybe_add_info_msg(
|
||||
context: &Context,
|
||||
failed: &DeliveryReport,
|
||||
chat_id: ChatId,
|
||||
chat_type: Chattype,
|
||||
) -> Result<()> {
|
||||
match chat_type {
|
||||
Chattype::Group | Chattype::Broadcast => {
|
||||
if let Some(failed_recipient) = &failed.failed_recipient {
|
||||
let contact_id =
|
||||
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
|
||||
.await?
|
||||
.context("contact ID not found")?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
// Tell the user which of the recipients failed if we know that (because in
|
||||
// a group, this might otherwise be unclear)
|
||||
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
|
||||
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
Chattype::Mailinglist => {
|
||||
// ndn_maybe_add_info_msg() is about the case when delivery to the group failed.
|
||||
// If we get an NDN for the mailing list, just issue a warning.
|
||||
warn!(context, "ignoring NDN for mailing list.");
|
||||
}
|
||||
Chattype::Single => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use super::*;
|
||||
@@ -3089,7 +3152,11 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
||||
|
||||
// Make sure the file is there even though the html is wrong:
|
||||
let param = &message.parts[0].param;
|
||||
let blob: BlobObject = param.get_blob(Param::File, &t).await.unwrap().unwrap();
|
||||
let blob: BlobObject = param
|
||||
.get_blob(Param::File, &t, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap();
|
||||
let size = f.metadata().await.unwrap().len();
|
||||
assert_eq!(size, 154);
|
||||
@@ -3587,10 +3654,9 @@ On 2020-10-25, Bob wrote:
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mime_modified_large_plain() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t1 = TestContext::new_alice().await;
|
||||
|
||||
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
|
||||
static REPEAT_CNT: usize = DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + 2;
|
||||
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN
|
||||
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
|
||||
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
|
||||
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
|
||||
@@ -3611,21 +3677,22 @@ On 2020-10-25, Bob wrote:
|
||||
if draft {
|
||||
chat.id.set_draft(&t, Some(&mut msg)).await?;
|
||||
}
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
t.send_msg(chat.id, &mut msg).await;
|
||||
let msg = t.get_last_msg_in(chat.id).await;
|
||||
assert!(msg.has_html());
|
||||
let html = msg.id.get_html(&t).await?.unwrap();
|
||||
assert_eq!(html.matches("<!DOCTYPE html>").count(), 1);
|
||||
assert_eq!(html.matches("just repeated.<br/>").count(), REPEAT_CNT);
|
||||
assert_eq!(
|
||||
msg.id
|
||||
.get_html(&t)
|
||||
.await?
|
||||
.unwrap()
|
||||
.matches("just repeated")
|
||||
.count(),
|
||||
REPEAT_CNT
|
||||
);
|
||||
assert!(
|
||||
msg.text.matches("just repeated.").count()
|
||||
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
|
||||
msg.text.matches("just repeated").count() <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
|
||||
);
|
||||
assert!(msg.text.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
|
||||
|
||||
let msg = t1.recv_msg(&sent_msg).await;
|
||||
assert!(msg.has_html());
|
||||
assert_eq!(msg.id.get_html(&t1).await?.unwrap(), html);
|
||||
}
|
||||
|
||||
t.set_config(Config::Bot, Some("1")).await?;
|
||||
|
||||
@@ -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`.
|
||||
///
|
||||
|
||||
284
src/net/http.rs
284
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::{create_id, time};
|
||||
|
||||
/// HTTP(S) GET response.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
/// Response body.
|
||||
pub blob: Vec<u8>,
|
||||
@@ -93,144 +90,9 @@ where
|
||||
Ok(sender)
|
||||
}
|
||||
|
||||
/// Converts the URL to expiration and stale timestamps.
|
||||
fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
|
||||
let now = time();
|
||||
|
||||
let expires = now + 3600 * 24 * 35;
|
||||
let stale = if url.ends_with(".xdc") {
|
||||
// WebXDCs are never stale, they just expire.
|
||||
expires
|
||||
} else if mimetype.is_some_and(|s| s.starts_with("image/")) {
|
||||
// Cache images for 1 day.
|
||||
//
|
||||
// As of 2024-12-12 WebXDC icons at <https://webxdc.org/apps/>
|
||||
// use the same path for all app versions,
|
||||
// so may change, but it is not critical if outdated icon is displayed.
|
||||
now + 3600 * 24
|
||||
} else {
|
||||
// Revalidate everything else after 1 hour.
|
||||
//
|
||||
// This includes HTML, CSS and JS.
|
||||
now + 3600
|
||||
};
|
||||
(expires, stale)
|
||||
}
|
||||
|
||||
/// Places the binary into HTTP cache.
|
||||
async fn http_cache_put(context: &Context, url: &str, response: &Response) -> Result<()> {
|
||||
let blob = BlobObject::create(
|
||||
context,
|
||||
&format!("http_cache_{}", create_id()),
|
||||
response.blob.as_slice(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (expires, stale) = http_url_cache_timestamps(url, response.mimetype.as_deref());
|
||||
context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT OR REPLACE INTO http_cache (url, expires, stale, blobname, mimetype, encoding)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
url,
|
||||
expires,
|
||||
stale,
|
||||
blob.as_name(),
|
||||
response.mimetype.as_deref().unwrap_or_default(),
|
||||
response.encoding.as_deref().unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves the binary from HTTP cache.
|
||||
///
|
||||
/// Also returns if the response is stale and should be revalidated in the background.
|
||||
async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response, bool)>> {
|
||||
let now = time();
|
||||
let Some((blob_name, mimetype, encoding, is_stale)) = 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, now > stale_timestamp))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
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.
|
||||
//
|
||||
// We do not update stale timestamp here
|
||||
// as we have not revalidated the response.
|
||||
// Stale timestamp is updated only
|
||||
// when the URL is sucessfully fetched.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE http_cache SET expires=? WHERE url=?",
|
||||
(expires, 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 {
|
||||
@@ -277,44 +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 stale result.
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
info!(context, "Not found {url:?} in cache, fetching.");
|
||||
let response = fetch_url(context, url).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Sends an empty POST request to the URL.
|
||||
///
|
||||
/// Returns response text and whether request was successful or not.
|
||||
@@ -407,109 +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);
|
||||
|
||||
// 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"))
|
||||
|
||||
23
src/param.rs
23
src/param.rs
@@ -366,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,
|
||||
@@ -382,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))
|
||||
@@ -538,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).
|
||||
@@ -78,14 +78,6 @@ impl Iroh {
|
||||
self.endpoint.network_change().await
|
||||
}
|
||||
|
||||
/// Closes the QUIC endpoint.
|
||||
pub(crate) async fn close(self) -> Result<()> {
|
||||
self.endpoint
|
||||
.close(0u32.into(), b"")
|
||||
.await
|
||||
.context("Closing iroh endpoint failed")
|
||||
}
|
||||
|
||||
/// Join a topic and create the subscriber loop for it.
|
||||
///
|
||||
/// If there is no gossip, create it.
|
||||
@@ -293,36 +285,15 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Get or initialize the iroh peer channel.
|
||||
pub async fn get_or_try_init_peer_channel(
|
||||
&self,
|
||||
) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
|
||||
pub async fn get_or_try_init_peer_channel(&self) -> Result<&Iroh> {
|
||||
if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
bail!("Attempt to get Iroh when realtime is disabled");
|
||||
}
|
||||
|
||||
if let Ok(lock) = tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
|
||||
self.iroh.read().await,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
) {
|
||||
return Ok(lock);
|
||||
}
|
||||
|
||||
let lock = self.iroh.write().await;
|
||||
match tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
|
||||
lock,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
) {
|
||||
Ok(lock) => Ok(lock),
|
||||
Err(mut lock) => {
|
||||
let iroh = self.init_peer_channels().await?;
|
||||
*lock = Some(iroh);
|
||||
tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
|
||||
lock,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
)
|
||||
.map_err(|_| anyhow!("Downgrade should succeed as we just stored `Some` value"))
|
||||
}
|
||||
}
|
||||
let ctx = self.clone();
|
||||
self.iroh
|
||||
.get_or_try_init(|| async { ctx.init_peer_channels().await })
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,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)
|
||||
@@ -664,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()
|
||||
@@ -689,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();
|
||||
@@ -711,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();
|
||||
@@ -742,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();
|
||||
@@ -773,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)]
|
||||
@@ -820,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)
|
||||
@@ -829,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()
|
||||
@@ -854,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();
|
||||
@@ -882,9 +811,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let bob_sequence_number = bob
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.get()
|
||||
.unwrap()
|
||||
.sequence_numbers
|
||||
.lock()
|
||||
@@ -893,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()
|
||||
@@ -904,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()
|
||||
@@ -914,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();
|
||||
@@ -935,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();
|
||||
@@ -958,9 +870,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(alice
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.get()
|
||||
.unwrap()
|
||||
.iroh_channels
|
||||
.read()
|
||||
@@ -1053,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.
|
||||
|
||||
@@ -121,7 +121,7 @@ impl Peerstate {
|
||||
last_seen_autocrypt: last_seen,
|
||||
prefer_encrypt,
|
||||
public_key: Some(public_key.clone()),
|
||||
public_key_fingerprint: Some(public_key.dc_fingerprint()),
|
||||
public_key_fingerprint: Some(public_key.fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_key_fingerprint: None,
|
||||
gossip_timestamp: 0,
|
||||
@@ -153,7 +153,7 @@ impl Peerstate {
|
||||
public_key: None,
|
||||
public_key_fingerprint: None,
|
||||
gossip_key: Some(gossip_header.public_key.clone()),
|
||||
gossip_key_fingerprint: Some(gossip_header.public_key.dc_fingerprint()),
|
||||
gossip_key_fingerprint: Some(gossip_header.public_key.fingerprint()),
|
||||
gossip_timestamp: message_time,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
@@ -308,7 +308,7 @@ impl Peerstate {
|
||||
pub fn recalc_fingerprint(&mut self) {
|
||||
if let Some(ref public_key) = self.public_key {
|
||||
let old_public_fingerprint = self.public_key_fingerprint.take();
|
||||
self.public_key_fingerprint = Some(public_key.dc_fingerprint());
|
||||
self.public_key_fingerprint = Some(public_key.fingerprint());
|
||||
|
||||
if old_public_fingerprint.is_some()
|
||||
&& old_public_fingerprint != self.public_key_fingerprint
|
||||
@@ -319,7 +319,7 @@ impl Peerstate {
|
||||
|
||||
if let Some(ref gossip_key) = self.gossip_key {
|
||||
let old_gossip_fingerprint = self.gossip_key_fingerprint.take();
|
||||
self.gossip_key_fingerprint = Some(gossip_key.dc_fingerprint());
|
||||
self.gossip_key_fingerprint = Some(gossip_key.fingerprint());
|
||||
|
||||
if old_gossip_fingerprint.is_none()
|
||||
|| self.gossip_key_fingerprint.is_none()
|
||||
@@ -506,7 +506,7 @@ impl Peerstate {
|
||||
fingerprint: Fingerprint,
|
||||
verifier: String,
|
||||
) -> Result<()> {
|
||||
if key.dc_fingerprint() == fingerprint {
|
||||
if key.fingerprint() == fingerprint {
|
||||
self.verified_key = Some(key);
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
@@ -524,7 +524,7 @@ impl Peerstate {
|
||||
/// do nothing to avoid overwriting secondary verified key
|
||||
/// which may be different.
|
||||
pub fn set_secondary_verified_key(&mut self, gossip_key: SignedPublicKey, verifier: String) {
|
||||
let fingerprint = gossip_key.dc_fingerprint();
|
||||
let fingerprint = gossip_key.fingerprint();
|
||||
if self.verified_key_fingerprint.as_ref() != Some(&fingerprint) {
|
||||
self.secondary_verified_key = Some(gossip_key);
|
||||
self.secondary_verified_key_fingerprint = Some(fingerprint);
|
||||
@@ -542,8 +542,6 @@ impl Peerstate {
|
||||
/// * `old_addr`: Old address of the peerstate in case of an AEAP transition.
|
||||
pub(crate) async fn save_to_db_ex(&self, sql: &Sql, old_addr: Option<&str>) -> Result<()> {
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let verified_key_fingerprint =
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex());
|
||||
if let Some(old_addr) = old_addr {
|
||||
// We are doing an AEAP transition to the new address and the SQL INSERT below will
|
||||
// save the existing peerstate as belonging to this new address. We now need to
|
||||
@@ -553,14 +551,11 @@ impl Peerstate {
|
||||
// existing peerstate as this would break encryption to it. This is critical for
|
||||
// non-verified groups -- if we can't encrypt to the old address, we can't securely
|
||||
// remove it from the group (to add the new one instead).
|
||||
//
|
||||
// NB: We check that `verified_key_fingerprint` hasn't changed to protect from
|
||||
// possible races.
|
||||
t.execute(
|
||||
"UPDATE acpeerstates
|
||||
SET verified_key=NULL, verified_key_fingerprint='', verifier=''
|
||||
WHERE addr=? AND verified_key_fingerprint=?",
|
||||
(old_addr, &verified_key_fingerprint),
|
||||
"UPDATE acpeerstates \
|
||||
SET verified_key=NULL, verified_key_fingerprint='', verifier='' \
|
||||
WHERE addr=?",
|
||||
(old_addr,),
|
||||
)?;
|
||||
}
|
||||
t.execute(
|
||||
@@ -609,7 +604,7 @@ impl Peerstate {
|
||||
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
&verified_key_fingerprint,
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verifier.as_deref().unwrap_or(""),
|
||||
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.secondary_verified_key_fingerprint
|
||||
@@ -893,12 +888,12 @@ mod tests {
|
||||
last_seen_autocrypt: 11,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
public_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
gossip_key: Some(pub_key.clone()),
|
||||
gossip_timestamp: 12,
|
||||
gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
gossip_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
verified_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
@@ -918,7 +913,7 @@ mod tests {
|
||||
.expect("no peerstate found in the database");
|
||||
|
||||
assert_eq!(peerstate, peerstate_new);
|
||||
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.dc_fingerprint())
|
||||
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
|
||||
.await
|
||||
.expect("failed to load peerstate from db")
|
||||
.expect("no peerstate found in the database");
|
||||
@@ -937,7 +932,7 @@ mod tests {
|
||||
last_seen_autocrypt: 11,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
public_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_timestamp: 12,
|
||||
gossip_key_fingerprint: None,
|
||||
@@ -974,7 +969,7 @@ mod tests {
|
||||
last_seen_autocrypt: 11,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
public_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_timestamp: 12,
|
||||
gossip_key_fingerprint: None,
|
||||
|
||||
91
src/pgp.rs
91
src/pgp.rs
@@ -14,16 +14,18 @@ use pgp::composed::{
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey};
|
||||
use pgp::types::{CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, StringToKey};
|
||||
use rand::{thread_rng, CryptoRng, Rng};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[cfg(test)]
|
||||
pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
@@ -39,15 +41,8 @@ enum SignedPublicKeyOrSubkey<'a> {
|
||||
Subkey(&'a SignedPublicSubKey),
|
||||
}
|
||||
|
||||
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
fn version(&self) -> pgp::types::KeyVersion {
|
||||
match self {
|
||||
Self::Key(k) => k.version(),
|
||||
Self::Subkey(k) => k.version(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fingerprint(&self) -> pgp::types::Fingerprint {
|
||||
impl KeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
fn fingerprint(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Self::Key(k) => k.fingerprint(),
|
||||
Self::Subkey(k) => k.fingerprint(),
|
||||
@@ -67,26 +62,14 @@ impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
Self::Subkey(k) => k.algorithm(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn created_at(&self) -> &chrono::DateTime<chrono::Utc> {
|
||||
match self {
|
||||
Self::Key(k) => k.created_at(),
|
||||
Self::Subkey(k) => k.created_at(),
|
||||
}
|
||||
}
|
||||
|
||||
fn expiration(&self) -> Option<u16> {
|
||||
match self {
|
||||
Self::Key(k) => k.expiration(),
|
||||
Self::Subkey(k) => k.expiration(),
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
fn verify_signature(
|
||||
&self,
|
||||
hash: HashAlgorithm,
|
||||
data: &[u8],
|
||||
sig: &SignatureBytes,
|
||||
sig: &[Mpi],
|
||||
) -> pgp::errors::Result<()> {
|
||||
match self {
|
||||
Self::Key(k) => k.verify_signature(hash, data, sig),
|
||||
@@ -96,27 +79,19 @@ impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
|
||||
fn encrypt<R: Rng + CryptoRng>(
|
||||
&self,
|
||||
rng: R,
|
||||
rng: &mut R,
|
||||
plain: &[u8],
|
||||
typ: pgp::types::EskType,
|
||||
) -> pgp::errors::Result<pgp::types::PkeskBytes> {
|
||||
) -> pgp::errors::Result<Vec<Mpi>> {
|
||||
match self {
|
||||
Self::Key(k) => k.encrypt(rng, plain, typ),
|
||||
Self::Subkey(k) => k.encrypt(rng, plain, typ),
|
||||
Self::Key(k) => k.encrypt(rng, plain),
|
||||
Self::Subkey(k) => k.encrypt(rng, plain),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_for_hashing(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
|
||||
fn to_writer_old(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
|
||||
match self {
|
||||
Self::Key(k) => k.serialize_for_hashing(writer),
|
||||
Self::Subkey(k) => k.serialize_for_hashing(writer),
|
||||
}
|
||||
}
|
||||
|
||||
fn public_params(&self) -> &pgp::types::PublicParams {
|
||||
match self {
|
||||
Self::Key(k) => k.public_params(),
|
||||
Self::Subkey(k) => k.public_params(),
|
||||
Self::Key(k) => k.to_writer_old(writer),
|
||||
Self::Subkey(k) => k.to_writer_old(writer),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,10 +160,9 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
|
||||
let (signing_key_type, encryption_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (
|
||||
PgpKeyType::EdDSALegacy,
|
||||
PgpKeyType::ECDH(ECCCurve::Curve25519),
|
||||
),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => {
|
||||
(PgpKeyType::EdDSA, PgpKeyType::ECDH(ECCCurve::Curve25519))
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = format!("<{addr}>");
|
||||
@@ -225,11 +199,10 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
|
||||
.build()
|
||||
.context("failed to build key parameters")?;
|
||||
|
||||
let mut rng = thread_rng();
|
||||
let secret_key = key_params
|
||||
.generate(&mut rng)
|
||||
.generate()
|
||||
.context("failed to generate the key")?
|
||||
.sign(&mut rng, || "".into())
|
||||
.sign(|| "".into())
|
||||
.context("failed to sign secret key")?;
|
||||
secret_key
|
||||
.verify()
|
||||
@@ -288,19 +261,15 @@ pub async fn pk_encrypt(
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
|
||||
let signed_msg = lit_msg.sign(&mut rng, skey, || "".into(), HASH_ALGORITHM)?;
|
||||
let signed_msg = lit_msg.sign(skey, || "".into(), HASH_ALGORITHM)?;
|
||||
let compressed_msg = if compress {
|
||||
signed_msg.compress(CompressionAlgorithm::ZLIB)?
|
||||
} else {
|
||||
signed_msg
|
||||
};
|
||||
compressed_msg.encrypt_to_keys_seipdv1(
|
||||
&mut rng,
|
||||
SYMMETRIC_KEY_ALGORITHM,
|
||||
&pkeys_refs,
|
||||
)?
|
||||
compressed_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
|
||||
} else {
|
||||
lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
|
||||
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
|
||||
};
|
||||
|
||||
let encoded_msg = encrypted_msg.to_armored_string(Default::default())?;
|
||||
@@ -315,9 +284,7 @@ pub fn pk_calc_signature(
|
||||
plain: &[u8],
|
||||
private_key_for_signing: &SignedSecretKey,
|
||||
) -> Result<String> {
|
||||
let mut rng = thread_rng();
|
||||
let msg = Message::new_literal_bytes("", plain).sign(
|
||||
&mut rng,
|
||||
private_key_for_signing,
|
||||
|| "".into(),
|
||||
HASH_ALGORITHM,
|
||||
@@ -361,7 +328,7 @@ pub fn valid_signature_fingerprints(
|
||||
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
|
||||
for pkey in public_keys_for_validation {
|
||||
if signed_msg.verify(&pkey.primary_key).is_ok() {
|
||||
let fp = pkey.dc_fingerprint();
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
ret_signature_fingerprints.insert(fp);
|
||||
}
|
||||
}
|
||||
@@ -388,7 +355,7 @@ pub fn pk_validate(
|
||||
|
||||
for pkey in public_keys_for_validation {
|
||||
if standalone_signature.verify(pkey, content).is_ok() {
|
||||
let fp = pkey.dc_fingerprint();
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
ret.insert(fp);
|
||||
}
|
||||
}
|
||||
@@ -403,12 +370,8 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut rng = thread_rng();
|
||||
let s2k = StringToKey::new_default(&mut rng);
|
||||
let msg = lit_msg.encrypt_with_password_seipdv1(
|
||||
&mut rng,
|
||||
s2k,
|
||||
SYMMETRIC_KEY_ALGORITHM,
|
||||
|| passphrase,
|
||||
)?;
|
||||
let msg =
|
||||
lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?;
|
||||
|
||||
let encoded_msg = msg.to_armored_string(Default::default())?;
|
||||
|
||||
|
||||
@@ -288,6 +288,8 @@ pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
|
||||
117
src/push.rs
117
src/push.rs
@@ -1,24 +1,10 @@
|
||||
//! # Push notifications module.
|
||||
//!
|
||||
//! This module is responsible for Apple Push Notification Service
|
||||
//! and Firebase Cloud Messaging push notifications.
|
||||
//!
|
||||
//! It provides [`PushSubscriber`] type
|
||||
//! which holds push notification token for the device,
|
||||
//! shared by all accounts.
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use pgp::crypto::aead::AeadAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::ser::Serialize;
|
||||
use rand::thread_rng;
|
||||
use anyhow::Result;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::key::DcKey;
|
||||
|
||||
/// Manages subscription to Apple Push Notification services.
|
||||
///
|
||||
@@ -38,85 +24,20 @@ pub struct PushSubscriber {
|
||||
inner: Arc<RwLock<PushSubscriberState>>,
|
||||
}
|
||||
|
||||
/// The key was generated with
|
||||
/// `rsop generate-key --profile rfc9580`
|
||||
/// and public key was extracted with `rsop extract-cert`.
|
||||
const NOTIFIERS_PUBLIC_KEY: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xioGZ03cdhsAAAAg6PasQQylEuWAp9N5PXN93rqjZdqOqN3s9RJEU/K8FZzCsAYf
|
||||
GwoAAABBBQJnTdx2AhsDAh4JCAsJCAcKDQwLBRUKCQgLAhYCIiEGiJJktnCmEtXa
|
||||
qsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAAUfgg/sg0sR2mytzADFBpNAaY0Hyu
|
||||
aru8ics3eUkeNn2ziL4ZsIMx+4mcM5POvD0PG9LtH8Rz/y9iItD0c2aoRBab7iri
|
||||
/gDm6aQuj3xXgtAiXdaN9s+QPxR9gY/zG1t9iXgBzioGZ03cdhkAAAAgwJ0wQFsk
|
||||
MGH4jklfK1fFhYoQZMjEFCRBIk+r1S+WaSDClQYYGwgAAAAsBQJnTdx2AhsMIiEG
|
||||
iJJktnCmEtXaqsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAKCRCIkmS2cKYS1WdP
|
||||
EFerccH2BoIPNbrxi6hwvxxy7G1mHg//ofD90fqmeY9xTfKMYl16bqQh4R1PiYd5
|
||||
LMc5VqgXHgioqTYKbltlOtWC+HDt/PrymQsN4q/aEmsM
|
||||
=5jvt
|
||||
-----END PGP PUBLIC KEY BLOCK-----";
|
||||
|
||||
/// Pads the token with spaces.
|
||||
///
|
||||
/// This makes it impossible to tell
|
||||
/// if the user is an Apple user with shorter tokens
|
||||
/// or FCM user with longer tokens by the length of ciphertext.
|
||||
fn pad_device_token(s: &str) -> String {
|
||||
// 512 is larger than any token, tokens seen so far have not been larger than 200 bytes.
|
||||
let expected_len: usize = 512;
|
||||
let payload_len = s.len();
|
||||
let padding_len = expected_len.saturating_sub(payload_len);
|
||||
let padding = " ".repeat(padding_len);
|
||||
let res = format!("{s}{padding}");
|
||||
debug_assert_eq!(res.len(), expected_len);
|
||||
res
|
||||
}
|
||||
|
||||
/// Encrypts device token with OpenPGP.
|
||||
///
|
||||
/// The result is base64-encoded and not ASCII armored to avoid dealing with newlines.
|
||||
pub(crate) fn encrypt_device_token(device_token: &str) -> Result<String> {
|
||||
let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0;
|
||||
let encryption_subkey = public_key
|
||||
.public_subkeys
|
||||
.first()
|
||||
.context("No encryption subkey found")?;
|
||||
let padded_device_token = pad_device_token(device_token);
|
||||
let literal_message = pgp::composed::Message::new_literal("", &padded_device_token);
|
||||
let mut rng = thread_rng();
|
||||
let chunk_size = 8;
|
||||
|
||||
let encrypted_message = literal_message.encrypt_to_keys_seipdv2(
|
||||
&mut rng,
|
||||
SymmetricKeyAlgorithm::AES128,
|
||||
AeadAlgorithm::Ocb,
|
||||
chunk_size,
|
||||
&[&encryption_subkey],
|
||||
)?;
|
||||
let encoded_message = encrypted_message.to_bytes()?;
|
||||
Ok(format!(
|
||||
"openpgp:{}",
|
||||
base64::engine::general_purpose::STANDARD.encode(encoded_message)
|
||||
))
|
||||
}
|
||||
|
||||
impl PushSubscriber {
|
||||
/// Creates new push notification subscriber.
|
||||
pub(crate) fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Sets device token for Apple Push Notification service
|
||||
/// or Firebase Cloud Messaging.
|
||||
/// Sets device token for Apple Push Notification service.
|
||||
pub(crate) async fn set_device_token(&self, token: &str) {
|
||||
self.inner.write().await.device_token = Some(token.to_string());
|
||||
}
|
||||
|
||||
/// Retrieves device token.
|
||||
///
|
||||
/// The token is encrypted with OpenPGP.
|
||||
///
|
||||
/// Token may be not available if application is not running on Apple platform,
|
||||
/// does not have Google Play services,
|
||||
/// failed to register for remote notifications or is in the process of registering.
|
||||
///
|
||||
/// IMAP loop should periodically check if device token is available
|
||||
@@ -200,37 +121,3 @@ impl Context {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_device_token() {
|
||||
let push_subscriber = PushSubscriber::new();
|
||||
assert_eq!(push_subscriber.device_token().await, None);
|
||||
|
||||
push_subscriber.set_device_token("some-token").await;
|
||||
let device_token = push_subscriber.device_token().await.unwrap();
|
||||
assert_eq!(device_token, "some-token");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pad_device_token() {
|
||||
let apple_token = "0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894";
|
||||
assert_eq!(pad_device_token(apple_token).trim(), apple_token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_device_token() {
|
||||
let fcm_token = encrypt_device_token("fcm-chat.delta:c67DVcpVQN2rJHiSszKNDW:APA91bErcJV2b8qG0IT4aiuCqw6Al0_SbydSuz3V0CHBR1X7Fp8YzyvlpxNZIOGYVDFKejZGE1YiGSaqxmkr9ds0DuALmZNDwqIhuZWGKKrs3r7DTSkQ9MQ").unwrap();
|
||||
let fcm_beta_token = encrypt_device_token("fcm-chat.delta.beta:chu-GhZCTLyzq1XseJp3na:APA91bFlsfDawdszWTyOLbxBy7KeRCrYM-SBFqutebF5ix0EZKMuCFUT_Y7R7Ex_eTQG_LbOu3Ky_z5UlTMJtI7ufpIp5wEvsFmVzQcOo3YhrUpbiSVGIlk").unwrap();
|
||||
let apple_token = encrypt_device_token(
|
||||
"0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(fcm_token.len(), fcm_beta_token.len());
|
||||
assert_eq!(apple_token.len(), fcm_token.len());
|
||||
}
|
||||
}
|
||||
|
||||
72
src/qr.rs
72
src/qr.rs
@@ -270,7 +270,6 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
|
||||
/// The function should be called after a QR code is scanned.
|
||||
/// The function takes the raw text scanned and checks what can be done with it.
|
||||
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let qr = qr.trim();
|
||||
let qrcode = if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
|
||||
decode_openpgp(context, qr)
|
||||
.await
|
||||
@@ -368,10 +367,9 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
|
||||
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(OPENPGP4FPR_SCHEME.len()..)
|
||||
.context("Invalid OPENPGP4FPR scheme")?;
|
||||
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
|
||||
|
||||
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
|
||||
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
|
||||
@@ -548,7 +546,7 @@ async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<
|
||||
fn decode_account(qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(DCACCOUNT_SCHEME.len()..)
|
||||
.context("Invalid DCACCOUNT payload")?;
|
||||
.context("invalid DCACCOUNT payload")?;
|
||||
let url = url::Url::parse(payload).context("Invalid account URL")?;
|
||||
if url.scheme() == "http" || url.scheme() == "https" {
|
||||
Ok(Qr::Account {
|
||||
@@ -566,7 +564,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
|
||||
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(DCWEBRTC_SCHEME.len()..)
|
||||
.context("Invalid DCWEBRTC payload")?;
|
||||
.context("invalid DCWEBRTC payload")?;
|
||||
|
||||
let (_type, url) = Message::parse_webrtc_instance(payload);
|
||||
let url = url::Url::parse(&url).context("Invalid WebRTC instance")?;
|
||||
@@ -639,7 +637,7 @@ fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
fn decode_backup2(qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.strip_prefix(DCBACKUP2_SCHEME)
|
||||
.ok_or_else(|| anyhow!("Invalid DCBACKUP2 scheme"))?;
|
||||
.ok_or_else(|| anyhow!("invalid DCBACKUP scheme"))?;
|
||||
let (auth_token, node_addr) = payload
|
||||
.split_once('&')
|
||||
.context("Backup QR code has no separator")?;
|
||||
@@ -670,10 +668,9 @@ struct CreateAccountErrorResponse {
|
||||
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
|
||||
/// download additional information from the contained url and set the parameters.
|
||||
/// on success, a configure::configure() should be able to log in to the account
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
let url_str = qr
|
||||
.get(DCACCOUNT_SCHEME.len()..)
|
||||
.context("Invalid DCACCOUNT scheme")?;
|
||||
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
if !url_str.starts_with(HTTPS_SCHEME) {
|
||||
bail!("DCACCOUNT QR codes must use HTTPS scheme");
|
||||
@@ -803,12 +800,15 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
/// Extract address for the mailto scheme.
|
||||
///
|
||||
/// Scheme: `mailto:addr...?subject=...&body=..`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(MAILTO_SCHEME.len()..)
|
||||
.context("Invalid mailto: scheme")?;
|
||||
let payload = &qr[MAILTO_SCHEME.len()..];
|
||||
|
||||
let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
|
||||
let (addr, query) = if let Some(query_index) = payload.find('?') {
|
||||
(&payload[..query_index], &payload[query_index + 1..])
|
||||
} else {
|
||||
(payload, "")
|
||||
};
|
||||
|
||||
let param: BTreeMap<&str, &str> = query
|
||||
.split('&')
|
||||
@@ -855,12 +855,16 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
/// Extract address for the smtp scheme.
|
||||
///
|
||||
/// Scheme: `SMTP:addr...:subject...:body...`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
|
||||
let payload = &qr[SMTP_SCHEME.len()..];
|
||||
|
||||
let addr = if let Some(query_index) = payload.find(':') {
|
||||
&payload[..query_index]
|
||||
} else {
|
||||
bail!("Invalid SMTP found");
|
||||
};
|
||||
|
||||
let (addr, _rest) = payload
|
||||
.split_once(':')
|
||||
.context("Invalid SMTP scheme payload")?;
|
||||
let addr = normalize_address(addr)?;
|
||||
let name = "";
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
@@ -871,13 +875,14 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
|
||||
///
|
||||
/// There may or may not be linebreaks after the fields.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
|
||||
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
|
||||
// we ignore this case.
|
||||
let addr = if let Some(to_index) = qr.find("TO:") {
|
||||
let addr = qr.get(to_index + 3..).unwrap_or_default().trim();
|
||||
let addr = qr[to_index + 3..].trim();
|
||||
if let Some(semi_index) = addr.find(';') {
|
||||
addr.get(..semi_index).unwrap_or_default().trim()
|
||||
addr[..semi_index].trim()
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
@@ -898,6 +903,7 @@ static VCARD_EMAIL_RE: Lazy<regex::Regex> =
|
||||
/// Extract address for the vcard scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let name = VCARD_NAME_RE
|
||||
.captures(qr)
|
||||
@@ -909,8 +915,8 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
|
||||
normalize_address(cap.as_str().trim())?
|
||||
let addr = if let Some(caps) = VCARD_EMAIL_RE.captures(qr) {
|
||||
normalize_address(caps[2].trim())?
|
||||
} else {
|
||||
bail!("Bad e-mail address");
|
||||
};
|
||||
@@ -995,17 +1001,6 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
// Test that QR code whitespace is stripped.
|
||||
// Users can copy-paste QR code contents and "scan"
|
||||
// from the clipboard.
|
||||
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1314,7 +1309,7 @@ mod tests {
|
||||
last_seen_autocrypt: 1,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
public_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_timestamp: 0,
|
||||
gossip_key_fingerprint: None,
|
||||
@@ -1345,10 +1340,7 @@ mod tests {
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
&format!(
|
||||
"OPENPGP4FPR:{}#a=alice@example.org",
|
||||
pub_key.dc_fingerprint()
|
||||
),
|
||||
&format!("OPENPGP4FPR:{}#a=alice@example.org", pub_key.fingerprint()),
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprOk { contact_id, .. } = qr {
|
||||
@@ -1755,9 +1747,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Test URL without port.
|
||||
//
|
||||
// Also check that whitespace is trimmed.
|
||||
let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await;
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user