.
+- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820))
+
+### CI
+
+- Build Python wheels for deltachat-rpc-server.
+
+### Build system
+
+- Strip release binaries.
+- Workaround OpenSSL crate expecting libatomic to be available.
+
+### Fixes
+
+- Set `soft_heap_limit` on SQLite database.
+- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT.
+
+## [1.125.0] - 2023-10-14
+
+### API-Changes
+
+- [**breaking**] deltachat-rpc-client: Replace `asyncio` with threads.
+- Validate boolean values passed to `set_config`. Attempts to set values other than `0` and `1` will result in an error.
+
+### CI
+
+- Reduce required Python version for deltachat-rpc-client from 3.8 to 3.7.
+
+### Features / Changes
+
+- Add developer option to disable IDLE.
+
+### Fixes
+
+- `deltachat-rpc-client`: Run `deltachat-rpc-server` in its own process group. This prevents reception of `SIGINT` by the server when the bot is terminated with `^C`.
+- python: Don't automatically set the displayname to "bot" when setting log level.
+- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/deltachat/deltachat-core-rust/pull/4700)).
+- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/deltachat/deltachat-core-rust/pull/4757)).
+- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/deltachat/deltachat-core-rust/pull/4811)).
+- Set connectivity status to "connected" during fake idle.
+- Return verifier contacts regardless of their origin.
+- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/deltachat/deltachat-core-rust/pull/4534)).
+
+### Refactor
+
+- deltachat-rpc-client: Close stdin instead of sending `SIGTERM`.
+- deltachat-rpc-client: Remove print() calls. Standard `logging` package is for logging instead.
+
+### Tests
+
+- deltachat-rpc-client: Enable logs in pytest.
+
+## [1.124.1] - 2023-10-05
+
+### Fixes
+
+- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)).
+
+### CI
+
+- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788))
+
+## [1.124.0] - 2023-10-04
+
+### API-Changes
+
+- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
+- Deprecate `dc_contact_get_verifier_addr`.
+- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
+- Deprecate `get_next_media()`.
+- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
+
+### Fixes
+
+- Wrap base64-encoded parts to 76 characters.
+- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
+- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
+- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
+- Do not put the status footer into reaction MIME parts.
+- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
+- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
+- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
+- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
+- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
+- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
+
+### Features / Changes
+
+- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
+- deltachat-rpc-client: Log exceptions when long-running tasks die.
+
+### Build
+
+- Build wheels for Python 3.12 and PyPy 3.10.
+
+## [1.123.0] - 2023-09-22
+
+### API-Changes
+
+- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
+- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
+
+### Fixes
+
+- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
+- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
+- imex: Use "default" in the filename of the default key.
+
+### Miscellaneous Tasks
+
+- Update OpenSSL from 3.1.2 to 3.1.3.
+
+## [1.122.0] - 2023-09-12
+
+### API-Changes
+
+- jsonrpc: Return only chat IDs for similar chats.
+
+### Fixes
+
+- Reopen all connections on database passpharse change.
+- Do not block new group chats if 1:1 chat is blocked.
+- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
+- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
+- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
+
+### Build system
+
+- Update to OpenSSL 3.0.
+- Bump webpki from 0.22.0 to 0.22.1.
+- python: Add link to Mastodon into projects.urls.
+
+### Features / Changes
+
+- Add RSA-4096 key generation support.
+
+### Refactor
+
+- pgp: Add constants for encryption algorithm and hash.
+
+## [1.121.0] - 2023-09-06
+
+### API-Changes
+
+- Add `dc_context_change_passphrase()`.
+- Add `Message.set_file_from_bytes()` API.
+- Add experimental API to get similar chats.
+
+### Build system
+
+- Build node packages on Ubuntu 18.04 instead of Debian 10.
+ This reduces the requirement for glibc version from 2.28 to 2.27.
+
+### Fixes
+
+- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
+- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
+- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
+- Do not allow dots at the end of email addresses.
+- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
+ `aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
+
+## [1.120.0] - 2023-08-28
+
+### API-Changes
+
+- jsonrpc: Add `resend_messages`.
+
+### Fixes
+
+- Update async-imap to 0.9.1 to fix memory leak.
+- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)).
+- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)).
+- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded.
+
+### Refactor
+
+- Hide accounts.rs constants from public API.
+- Hide pgp module from public API.
+
+### Build system
+
+- Update to Zig 0.11.0.
+- Update to Rust 1.72.0.
+
+### CI
+
+- Run on push to stable branch.
+
+### Miscellaneous Tasks
+
+- python: Fix lint errors.
+- python: Fix `ruff` 0.0.286 warnings.
+- Fix beta clippy warnings.
+
+## [1.119.1] - 2023-08-06
+
+Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610).
+
+### Features / Changes
+
+- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)).
+
+### Fixes
+
+- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import.
+
+### Tests
+
+- webxdc: Ensure unknown WebXDC update properties do not result in an error.
+
+## [1.119.0] - 2023-08-03
+
+### Fixes
+
+- imap: Avoid IMAP move loops when DeltaChat folder is aliased.
+- imap: Do not resync IMAP after initial configuration.
+
+- webxdc: Accept WebXDC updates in mailing lists.
+- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates.
+- webxdc: Delete old webxdc status updates during housekeeping.
+
+- Return valid MsgId from `receive_imf()` when the message is replaced.
+- Emit MsgsChanged event with correct chat id for replaced messages.
+
+- deltachat-rpc-server: Update tokio-tar to fix backup import.
+
+### Features / Changes
+
+- deltachat-rpc-client: Add `MSG_DELETED` constant.
+- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
+
+### API-Changes
+
+- deltachat-rpc-client: Add `Account.{import,export}_backup` methods.
+- deltachat-jsonrpc: Make `MessageObject.text` non-optional.
+
+### Documentation
+
+- Update default value for `show_emails` in `dc_set_config()` documentation.
+
+### Refactor
+
+- Improve IMAP logs.
+
+### Tests
+
+- Add basic import/export test for async python.
+- Add `test_webxdc_download_on_demand`.
+- Add tests for deletion of webxdc status-updates.
+
+## [1.118.0] - 2023-07-07
+
+### API-Changes
+
+- [**breaking**] Remove `Contact::load_from_db()` in favor of `Contact::get_by_id()`.
+- Add `Contact::get_by_id_optional()` API.
+- [**breaking**] Make `Message.text` non-optional.
+- [**breaking**] Replace `message::get_msg_info()` with `MsgId.get_info()`.
+- Move `handle_mdn` and `handle_ndn` to mimeparser and make them private.
+ Previously `handle_mdn` was erroneously exposed in the public API.
+- python: flatten the API of `deltachat` module.
+
+### Fixes
+
+- Use different member added/removal messages locally and on the network.
+- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)).
+- Make SVG avatar image work on more platforms (use `xlink:href`).
+- Preserve indentation when converting plaintext to HTML.
+- Do not run simplify() on dehtml() output.
+- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)).
+
+### Documentation
+
+- Document how to regenerate Node.js constants before the release.
+
+### Build system
+
+- git-cliff: Do not fail if commit.footers is undefined.
+
+### Other
+
+- Dependency updates.
+- Update MPL 2.0 license text.
+- Add LICENSE file to deltachat-rpc-client.
+- deltachat-rpc-client: Add Trove classifiers.
+- python: Change bindings status to production/stable.
+
+### Tests
+
+- Add `make-python-testenv.sh` script.
+
+## [1.117.0] - 2023-06-15
+
+### Features
+
+- New group membership update algorithm.
+
+ New algorithm improves group consistency
+ in cases of missing messages,
+ restored old backups and replies from classic MUAs.
+
+- Add `DC_EVENT_MSG_DELETED` event.
+
+ This event notifies the UI about the message
+ being deleted from the messagelist, e.g. when the message expires
+ or the user deletes it.
+
+### Fixes
+
+- Emit `DC_EVENT_MSGS_CHANGED` without IDs when the message expires.
+
+ Specifying msg IDs that cannot be loaded in the event payload
+ results in an error when the UI tries to load the message.
+ Instead, emit an event without IDs
+ to make the UI reload the whole messagelist.
+
+- Ignore address case when comparing the `To:` field to `Autocrypt-Gossip:`.
+
+ This bug resulted in failure to propagate verification
+ if the contact list already contained a new verified group member
+ with a non-lowercase address.
+
+- dehtml: skip links with empty text.
+
+ Links like ` ` in HTML mails are now skipped
+ instead of being converted to a link without a label like `[](https://delta.chat/)`.
+
+- dehtml: Do not insert unnecessary newlines when parsing `` tags.
+
+- Update from yanked `libc` 0.2.145 to 0.2.146.
+- Update to async-imap 0.9.0 to remove deprecated `ouroboros` dependency.
+
+### API-Changes
+
+- Emit `DC_EVENT_MSGS_CHANGED` per chat when messages are deleted.
+
+ Previously a single event with zero chat ID was emitted.
+
+- python: make `Contact.is_verified()` return bool.
+
+- rust: add API endpoint `get_status_update` ([#4468](https://github.com/deltachat/deltachat-core-rust/pull/4468)).
+
+- rust: make `WebxdcManifest` type public.
+
+### Build system
+
+- Use Rust 1.70.0 to compile deltachat-rpc-server releases.
+- Disable unused `brotli` feature `ffi-api` and use 1 codegen-units for release builds to reduce the size of the binaries.
+
+### CI
+
+- Run `cargo check` with musl libc.
+- concourse: Install devpi in a virtual environment.
+- Remove [mergeable](https://mergeable.us/) configuration.
+
+### Documentation
+
+- README: mark napi.rs bindings as experimental. CFFI bindings are not legacy and are the recommended Node.js bindings currently.
+- CONTRIBUTING: document how conventional commits interact with squash merges.
+
+### Refactor
+
+- Rename `MimeMessage.header` into `MimeMessage.headers`.
+
+- Derive `Default` trait for `WebxdcManifest`.
+
+### Tests
+
+- Regression test for case-sensitive comparison of gossip header to contact address.
+- Multiple new group consistency tests in Rust.
+- python: Replace legacy `tmpdir` fixture with `tmp_path`.
+
+## [1.116.0] - 2023-06-05
+
+### API-Changes
+
+- Add `dc_jsonrpc_blocking_call()`.
+
+### Changes
+
+- Generate OpenRPC definitions for JSON-RPC.
+- Add more context to message loading errors.
+
+### Fixes
+
+- Build deltachat-node prebuilds on Debian 10.
+
+### Documentation
+
+- Document release process in `RELEASE.md`.
+- Add contributing guidelines `CONTRIBUTING.md`.
+- Update instructions for python devenv.
+- python: Document pytest fixtures.
+
+### Tests
+
+- python: Make `test_mdn_asymmetric` less flaky.
+- Make `test_group_with_removed_message_id` less flaky.
+- Add golden tests infrastructure ([#4395](https://github.com/deltachat/deltachat-core-rust/pull/4395)).
+
+### Build system
+
+- git-cliff: Changelog generation improvements.
+- `set_core_version.py`: Expect release date in the changelog.
+
+### CI
+
+- Require Python 3.8 for deltachat-rpc-client.
+- mergeable: Allow PR titles to start with "ci" and "build".
+- Remove incorrect comment.
+- dependabot: Use `chore` prefix for dependency updates.
+- Remove broken `node-delete-preview.yml` workflow.
+- Add top comments to GH Actions workflows.
+- Run node.js lint on Windows.
+- Update clippy to 1.70.0.
+
+### Miscellaneous Tasks
+
+- Remove release.toml.
+- gitattributes: Configure LF line endings for JavaScript files.
+- Update dependencies
+
+## [1.112.10] - 2023-06-01
+
+### Fixes
+
+- Disable `fetch_existing_msgs` setting by default.
+- Update `h2` to fix RUSTSEC-2023-0034.
+
+## [1.115.0] - 2023-05-12
+
+### JSON-RPC API Changes
+
+- Sort reactions in descending order ([#4388](https://github.com/deltachat/deltachat-core-rust/pull/4388)).
+- Add API to get reactions outside the message snapshot.
+- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`.
+- `get_chatlist_entries` now returns `Vec` of chatids instead of `ChatListEntries`.
+- `JSONRPCReactions.reactions` is now a `Vec` with unique reactions and their count, sorted in descending order.
+- `Event`: `context_id` property is now called `contextId`.
+- Expand `MessageSearchResult`:
+ - Always include `chat_name`(not an option anymore).
+ - Add `author_id`, `chat_type`, `chat_color`, `is_chat_protected`, `is_chat_contact_request`, `is_chat_archived`.
+ - `author_name` now contains the overridden sender name.
+- `ChatListItemFetchResult` gets new properties: `summary_preview_image`, `last_message_type` and `last_message_id`
+- New `MessageReadReceipt` type and `get_message_read_receipts(account_id, message_id)` jsonrpc method.
+
+### API Changes
+
+- New rust API `send_webxdc_status_update_struct` to send a `StatusUpdateItem`.
+- Add `get_msg_read_receipts(context, msg_id)` - get the contacts that send read receipts for a message.
+
+### Features / Changes
+
+- Build deltachat-rpc-server releases for x86\_64 macOS.
+- Generate changelogs using git-cliff ([#4393](https://github.com/deltachat/deltachat-core-rust/pull/4393), [#4396](https://github.com/deltachat/deltachat-core-rust/pull/4396)).
+- Improve SMTP logging.
+- Do not cut incoming text if "bot" config is set.
+
+### Fixes
+
+- JSON-RPC: typescript client: fix types of events in event emitter ([#4373](https://github.com/deltachat/deltachat-core-rust/pull/4373)).
+- Fetch at most 100 existing messages even if EXISTS was not received ([#4383](https://github.com/deltachat/deltachat-core-rust/pull/4383)).
+- Don't put a double dot at the end of error messages ([#4398](https://github.com/deltachat/deltachat-core-rust/pull/4398)).
+- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/deltachat/deltachat-core-rust/pull/4390)).
+- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
+- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/deltachat/deltachat-core-rust/pull/4377)).
+- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/deltachat/deltachat-core-rust/pull/4391)).
+
+### Refactor
+
+- Iterate over `msg_ids` without .iter().
+
+## [1.112.9] - 2023-05-12
+
+### Fixes
+
+- Fetch at most 100 existing messages even if EXISTS was not received.
+- Delete `smtp` rows when message sending is cancelled.
+
+### Changes
+
+- Improve SMTP logging.
+
+## [1.114.0] - 2023-04-24
+
+### Changes
+- JSON-RPC: Use long polling instead of server-sent notifications to retrieve events.
+ This better corresponds to JSON-RPC 2.0 server-client distinction
+ and is expected to simplify writing new bindings
+ because dispatching events can be done on higher level.
+- JSON-RPC: TS: Client now has a mandatory argument whether you want to start listening for events.
+
+### Fixes
+- JSON-RPC: do not print to stdout on failure to find an account.
+
+
+## [1.113.0] - 2023-04-18
+
+### Added
+- New JSON-RPC API `can_send()`.
+- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
+ New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
+ These APIs can be used by bots to get all unprocessed messages
+ in the order of their arrival and wait for them without relying on events.
+- New Python bindings API `Account.wait_next_incoming_message()`.
+- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
### Changes
- Increase MSRV to 1.65.0. #4236
- Remove upper limit on the attachment size. #4253
- Update rPGP to 0.10.1. #4236
-- Compress `mime_headers` column with HTML emails stored in database
-- Strip BIDI characters in system messages, files, group names and contact names #3479
-- maybe_add_time_based_warnings(): Use release date instead of the provider DB update one
-- Remove confusing log line "ignoring unsolicited response Recent(…)" #3934
-- Cleanly terminate deltachat-rpc-server.
- Also terminate on ctrl-c.
-- Refactorings #4317
-- Add JSON-RPC API `can_send()`.
-- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
- New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
- These APIs can be used by bots to get all unprocessed messages
- in the order of their arrival and wait for them without relying on events.
+- Compress HTML emails stored in the `mime_headers` column of the database.
+- Strip BIDI characters in system messages, files, group names and contact names. #3479
+- Use release date instead of the provider database update date in `maybe_add_time_based_warnings()`.
+- Gracefully terminate `deltachat-rpc-server` on Ctrl+C (`SIGINT`), `SIGTERM` and EOF.
- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated
in favor of `get_next_msgs()` and `wait_next_msgs()`.
-- New Python bindings API `Account.wait_next_incoming_message()`.
-- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
+- Remove metadata from avatars and JPEG images before sending. #4037
+- Recode PNG and other supported image formats to JPEG if they are > 500K in size. #4037
### Fixes
+- Don't let blocking be bypassed using groups. #4316
+- Show a warning if quota list is empty. #4261
+- Do not reset status on other devices when sending signed reaction messages. #3692
+- Update `accounts.toml` atomically.
- Fix python bindings README documentation on installing the bindings from source.
-- Show a warning if quota list is empty #4261
-- Update "accounts.toml" atomically
-- Don't let blocking be bypassed using groups #4316
+- Remove confusing log line "ignoring unsolicited response Recent(…)". #3934
+
+## [1.112.8] - 2023-04-20
+
+### Changes
+- Add `get_http_response` JSON-RPC API.
+- Add C API to get HTTP responses.
+
+## [1.112.7] - 2023-04-17
+
+### Fixes
+
+- Updated `async-imap` to v0.8.0 to fix erroneous EOF detection in long IMAP responses.
## [1.112.6] - 2023-04-04
@@ -2393,7 +2939,6 @@ For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
-[unreleased]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...HEAD
[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0
[1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0
[1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1
@@ -2401,3 +2946,24 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3
[1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4
[1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5
+[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6
+[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
+[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
+[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9
+[1.112.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.112.10
+[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0
+[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
+[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0
+[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
+[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
+[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0
+[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
+[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
+[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
+[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
+[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
+[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
+[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
+[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
+[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
+[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..0285b2697
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,115 @@
+# Contributing guidelines
+
+## 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
+[Android](https://github.com/deltachat/deltachat-android/issues),
+[iOS](https://github.com/deltachat/deltachat-ios/issues) or
+[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
+report it to the corresponding repository.
+
+## Proposing features
+
+If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
+
+## Contributing code
+
+If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
+
+If you have write access to the repository,
+push a branch named `/`
+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:
+
+### 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].
+
+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}"))
+```
+
+### 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).
diff --git a/Cargo.lock b/Cargo.lock
index 89b75936b..b8ae2c8af 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,12 +2,6 @@
# It is not intended for manual editing.
version = 3
-[[package]]
-name = "Inflector"
-version = "0.11.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
-
[[package]]
name = "abao"
version = "0.2.0"
@@ -23,9 +17,9 @@ dependencies = [
[[package]]
name = "addr2line"
-version = "0.19.0"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
@@ -38,9 +32,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aes"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241"
+checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
@@ -49,30 +43,25 @@ dependencies = [
[[package]]
name = "ahash"
-version = "0.7.6"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d"
dependencies = [
- "getrandom 0.2.8",
+ "cfg-if",
"once_cell",
"version_check",
+ "zerocopy",
]
[[package]]
name = "aho-corasick"
-version = "0.7.20"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
dependencies = [
"memchr",
]
-[[package]]
-name = "aliasable"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
-
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
@@ -88,6 +77,18 @@ dependencies = [
"alloc-no-stdlib",
]
+[[package]]
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -113,25 +114,31 @@ dependencies = [
]
[[package]]
-name = "anyhow"
-version = "1.0.70"
+name = "anstyle"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
+checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
+
+[[package]]
+name = "anyhow"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
dependencies = [
"backtrace",
]
[[package]]
name = "arrayref"
-version = "0.3.6"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
[[package]]
name = "arrayvec"
-version = "0.7.2"
+version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "ascii_utils"
@@ -141,9 +148,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]]
name = "asn1-rs"
-version = "0.5.1"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf6690c370453db30743b373a60ba498fc0d6d83b11f4abfd87a84a075db5dd4"
+checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
@@ -152,7 +159,7 @@ dependencies = [
"num-traits",
"rusticata-macros",
"thiserror",
- "time 0.3.20",
+ "time 0.3.24",
]
[[package]]
@@ -180,9 +187,9 @@ dependencies = [
[[package]]
name = "async-channel"
-version = "1.8.0"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
@@ -204,22 +211,21 @@ dependencies = [
[[package]]
name = "async-imap"
-version = "0.7.0"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8379e2f1cdeb79afd2006932d7e8f64993fc0f7386d0ebc37231c90b05968c25"
+checksum = "936c1b580be4373b48c9c687e0c79285441664398354df28d0860087cac0c069"
dependencies = [
"async-channel",
- "async-native-tls 0.4.0",
- "base64 0.21.0",
- "byte-pool",
+ "base64 0.21.3",
+ "bytes",
"chrono",
"futures",
"imap-proto",
"log",
"nom",
"once_cell",
- "ouroboros",
"pin-utils",
+ "self_cell",
"stop-token",
"thiserror",
"tokio",
@@ -234,18 +240,6 @@ dependencies = [
"event-listener",
]
-[[package]]
-name = "async-native-tls"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe"
-dependencies = [
- "native-tls",
- "thiserror",
- "tokio",
- "url",
-]
-
[[package]]
name = "async-native-tls"
version = "0.5.0"
@@ -277,13 +271,13 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.68"
+version = "0.1.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
+checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.13",
+ "syn 2.0.29",
]
[[package]]
@@ -300,17 +294,6 @@ dependencies = [
"tokio",
]
-[[package]]
-name = "atty"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
-dependencies = [
- "hermit-abi 0.1.19",
- "libc",
- "winapi",
-]
-
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -319,13 +302,13 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
-version = "0.6.12"
+version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "349f8ccfd9221ee7d1f3d4b33e1f8319b3a81ed8f61f2ea40b37b859794b4491"
+checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core",
- "base64 0.21.0",
+ "base64 0.21.3",
"bitflags 1.3.2",
"bytes",
"futures-util",
@@ -354,9 +337,9 @@ dependencies = [
[[package]]
name = "axum-core"
-version = "0.3.3"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2f958c80c248b34b9a877a643811be8dbca03ca5ba827f2b63baf3a81e5fc4e"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
dependencies = [
"async-trait",
"bytes",
@@ -371,9 +354,9 @@ dependencies = [
[[package]]
name = "backtrace"
-version = "0.3.67"
+version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
@@ -416,9 +399,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
-version = "0.21.0"
+version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
+checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
[[package]]
name = "base64ct"
@@ -449,22 +432,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.0.2"
+version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
+checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
[[package]]
name = "blake3"
-version = "1.3.3"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
+checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
- "digest 0.10.6",
+ "digest 0.10.7",
]
[[package]]
@@ -478,18 +461,18 @@ dependencies = [
[[package]]
name = "block-buffer"
-version = "0.10.3"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
-version = "0.3.2"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
@@ -506,9 +489,9 @@ dependencies = [
[[package]]
name = "brotli"
-version = "3.3.4"
+version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -517,9 +500,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
-version = "2.3.4"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
+checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -527,9 +510,9 @@ dependencies = [
[[package]]
name = "bstr"
-version = "1.4.0"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
+checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"serde",
@@ -547,19 +530,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.12.0"
+version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
-
-[[package]]
-name = "byte-pool"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca"
-dependencies = [
- "crossbeam-queue",
- "stable_deref_trait",
-]
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "bytemuck"
@@ -591,18 +564,18 @@ dependencies = [
[[package]]
name = "camino"
-version = "1.1.3"
+version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6031a462f977dd38968b6f23378356512feeace69cef817e1a4475108093cec3"
+checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
-version = "0.1.2"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27"
+checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479"
dependencies = [
"serde",
]
@@ -637,9 +610,12 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.0.79"
+version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+checksum = "51f1226cd9da55587234753d1245dd5b132343ea240f26b6a9003d68706141ba"
+dependencies = [
+ "libc",
+]
[[package]]
name = "cfb-mode"
@@ -668,24 +644,24 @@ dependencies = [
[[package]]
name = "chrono"
-version = "0.4.24"
+version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
+checksum = "95ed24df0632f708f5f6d8082675bef2596f7084dee3dd55f632290bf35bfe0f"
dependencies = [
+ "android-tzdata",
"iana-time-zone",
"js-sys",
- "num-integer",
"num-traits",
"time 0.1.45",
"wasm-bindgen",
- "winapi",
+ "windows-targets 0.48.1",
]
[[package]]
name = "ciborium"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
+checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926"
dependencies = [
"ciborium-io",
"ciborium-ll",
@@ -694,15 +670,15 @@ dependencies = [
[[package]]
name = "ciborium-io"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
+checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656"
[[package]]
name = "ciborium-ll"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
+checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b"
dependencies = [
"ciborium-io",
"half",
@@ -710,9 +686,9 @@ dependencies = [
[[package]]
name = "cipher"
-version = "0.4.3"
+version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
@@ -720,24 +696,28 @@ dependencies = [
[[package]]
name = "clap"
-version = "3.2.23"
+version = "4.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d"
dependencies = [
- "bitflags 1.3.2",
+ "clap_builder",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1"
+dependencies = [
+ "anstyle",
"clap_lex",
- "indexmap",
- "textwrap",
]
[[package]]
name = "clap_lex"
-version = "0.2.4"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
-dependencies = [
- "os_str_bytes",
-]
+checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "clipboard-win"
@@ -756,16 +736,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
-[[package]]
-name = "codespan-reporting"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
-dependencies = [
- "termcolor",
- "unicode-width",
-]
-
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -774,33 +744,33 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "concurrent-queue"
-version = "2.1.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e"
+checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
-version = "0.9.2"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913"
+checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747"
[[package]]
name = "const_format"
-version = "0.2.30"
+version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e"
+checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
-version = "0.2.29"
+version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650"
+checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6"
dependencies = [
"proc-macro2",
"quote",
@@ -809,9 +779,9 @@ dependencies = [
[[package]]
name = "constant_time_eq"
-version = "0.2.4"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279"
+checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
[[package]]
name = "convert_case"
@@ -837,15 +807,15 @@ dependencies = [
[[package]]
name = "core-foundation-sys"
-version = "0.8.3"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
-version = "0.2.5"
+version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
@@ -867,20 +837,20 @@ dependencies = [
[[package]]
name = "criterion"
-version = "0.4.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
- "atty",
"cast",
"ciborium",
"clap",
"criterion-plot",
"futures",
+ "is-terminal",
"itertools",
- "lazy_static",
"num-traits",
+ "once_cell",
"oorandom",
"plotters",
"rayon",
@@ -926,9 +896,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
-version = "0.9.14"
+version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
dependencies = [
"autocfg",
"cfg-if",
@@ -937,21 +907,11 @@ dependencies = [
"scopeguard",
]
-[[package]]
-name = "crossbeam-queue"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
-dependencies = [
- "cfg-if",
- "crossbeam-utils",
-]
-
[[package]]
name = "crossbeam-utils"
-version = "0.8.15"
+version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if",
]
@@ -970,9 +930,9 @@ dependencies = [
[[package]]
name = "crypto-bigint"
-version = "0.5.1"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c2538c4e68e52548bacb3e83ac549f903d44f011ac9d5abb5e132e67d0808f7"
+checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15"
dependencies = [
"generic-array",
"rand_core 0.6.4",
@@ -1005,61 +965,30 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
-version = "4.0.0-rc.2"
+version = "4.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585"
+checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
dependencies = [
"cfg-if",
- "digest 0.10.6",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest 0.10.7",
"fiat-crypto",
- "packed_simd_2",
"platforms",
+ "rustc_version",
"subtle",
"zeroize",
]
[[package]]
-name = "cxx"
-version = "1.0.91"
+name = "curve25519-dalek-derive"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62"
-dependencies = [
- "cc",
- "cxxbridge-flags",
- "cxxbridge-macro",
- "link-cplusplus",
-]
-
-[[package]]
-name = "cxx-build"
-version = "1.0.91"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690"
-dependencies = [
- "cc",
- "codespan-reporting",
- "once_cell",
- "proc-macro2",
- "quote",
- "scratch",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "cxxbridge-flags"
-version = "1.0.91"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf"
-
-[[package]]
-name = "cxxbridge-macro"
-version = "1.0.91"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892"
+checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
]
[[package]]
@@ -1074,12 +1003,12 @@ dependencies = [
[[package]]
name = "darling"
-version = "0.14.3"
+version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0808e1bd8671fb44a113a14e13497557533369847788fa2ae912b6ebfce9fa8"
+checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
dependencies = [
- "darling_core 0.14.3",
- "darling_macro 0.14.3",
+ "darling_core 0.14.4",
+ "darling_macro 0.14.4",
]
[[package]]
@@ -1098,9 +1027,9 @@ dependencies = [
[[package]]
name = "darling_core"
-version = "0.14.3"
+version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "001d80444f28e193f30c2f293455da62dcf9a6b29918a4253152ae2b1de592cb"
+checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [
"fnv",
"ident_case",
@@ -1123,20 +1052,20 @@ dependencies = [
[[package]]
name = "darling_macro"
-version = "0.14.3"
+version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b36230598a2d5de7ec1c6f51f72d8a99a9208daff41de2084d06e3fd3ea56685"
+checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
- "darling_core 0.14.3",
+ "darling_core 0.14.4",
"quote",
"syn 1.0.109",
]
[[package]]
name = "data-encoding"
-version = "2.3.3"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
+checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "default-net"
@@ -1152,22 +1081,22 @@ dependencies = [
"netlink-sys",
"once_cell",
"system-configuration",
- "windows",
+ "windows 0.32.0",
]
[[package]]
name = "deltachat"
-version = "1.112.6"
+version = "1.126.1"
dependencies = [
"ansi_term",
"anyhow",
"async-channel",
"async-imap",
- "async-native-tls 0.5.0",
+ "async-native-tls",
"async-smtp",
"async_zip",
"backtrace",
- "base64 0.21.0",
+ "base64 0.21.3",
"brotli",
"chrono",
"criterion",
@@ -1176,10 +1105,12 @@ dependencies = [
"encoded-words",
"escaper",
"fast-socks5",
+ "fd-lock",
"format-flowed",
"futures",
"futures-lite",
"hex",
+ "hickory-resolver",
"humansize",
"image",
"iroh",
@@ -1188,6 +1119,7 @@ dependencies = [
"libc",
"log",
"mailparse",
+ "mime",
"num-derive",
"num-traits",
"num_cpus",
@@ -1195,6 +1127,7 @@ dependencies = [
"parking_lot",
"percent-encoding",
"pgp",
+ "pretty_assertions",
"pretty_env_logger",
"proptest",
"qrcodegen",
@@ -1209,7 +1142,7 @@ dependencies = [
"serde",
"serde_json",
"sha-1",
- "sha2 0.10.6",
+ "sha2 0.10.8",
"smallvec",
"strum",
"strum_macros",
@@ -1224,25 +1157,25 @@ dependencies = [
"tokio-tar",
"tokio-util",
"toml",
- "trust-dns-resolver",
"url",
"uuid",
]
[[package]]
name = "deltachat-jsonrpc"
-version = "1.112.6"
+version = "1.126.1"
dependencies = [
"anyhow",
"async-channel",
"axum",
- "base64 0.21.0",
+ "base64 0.21.3",
"deltachat",
- "env_logger 0.10.0",
+ "env_logger",
"futures",
"log",
"num-traits",
"sanitize-filename",
+ "schemars",
"serde",
"serde_json",
"tempfile",
@@ -1254,7 +1187,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
-version = "1.112.6"
+version = "1.126.1"
dependencies = [
"ansi_term",
"anyhow",
@@ -1269,12 +1202,12 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
-version = "1.112.6"
+version = "1.126.1"
dependencies = [
"anyhow",
"deltachat",
"deltachat-jsonrpc",
- "env_logger 0.10.0",
+ "env_logger",
"futures-lite",
"log",
"serde",
@@ -1289,12 +1222,12 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
- "syn 2.0.13",
+ "syn 2.0.29",
]
[[package]]
name = "deltachat_ffi"
-version = "1.112.6"
+version = "1.126.1"
dependencies = [
"anyhow",
"deltachat",
@@ -1307,6 +1240,7 @@ dependencies = [
"serde_json",
"thiserror",
"tokio",
+ "yerpc",
]
[[package]]
@@ -1323,9 +1257,9 @@ dependencies = [
[[package]]
name = "der"
-version = "0.7.1"
+version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc906908ea6458456e5eaa160a9c08543ec3d1e6f71e2235cedd660cb65f9df0"
+checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946"
dependencies = [
"const-oid",
"pem-rfc7468 0.7.0",
@@ -1334,9 +1268,9 @@ dependencies = [
[[package]]
name = "der-parser"
-version = "8.1.0"
+version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42d4bc9b0db0a0df9ae64634ac5bdefb7afcb534e182275ca0beadbe486701c1"
+checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e"
dependencies = [
"asn1-rs",
"displaydoc",
@@ -1358,6 +1292,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "deranged"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01"
+
[[package]]
name = "derive_builder"
version = "0.12.0"
@@ -1373,7 +1313,7 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
dependencies = [
- "darling 0.14.3",
+ "darling 0.14.4",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -1411,6 +1351,12 @@ dependencies = [
"cipher",
]
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
[[package]]
name = "digest"
version = "0.9.0"
@@ -1422,11 +1368,11 @@ dependencies = [
[[package]]
name = "digest"
-version = "0.10.6"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
- "block-buffer 0.10.3",
+ "block-buffer 0.10.4",
"const-oid",
"crypto-common",
"subtle",
@@ -1434,9 +1380,9 @@ dependencies = [
[[package]]
name = "dirs"
-version = "5.0.0"
+version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
@@ -1453,13 +1399,14 @@ dependencies = [
[[package]]
name = "dirs-sys"
-version = "0.4.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
+ "option-ext",
"redox_users",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -1475,13 +1422,13 @@ dependencies = [
[[package]]
name = "displaydoc"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
]
[[package]]
@@ -1507,6 +1454,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "dyn-clone"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
+
[[package]]
name = "ecdsa"
version = "0.14.8"
@@ -1521,14 +1474,16 @@ dependencies = [
[[package]]
name = "ecdsa"
-version = "0.16.2"
+version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "644d3b8674a5fc5b929ae435bca85c2323d85ccb013a5509c2ac9ee11a6284ba"
+checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
dependencies = [
- "der 0.7.1",
- "elliptic-curve 0.13.2",
+ "der 0.7.7",
+ "digest 0.10.7",
+ "elliptic-curve 0.13.5",
"rfc6979 0.4.0",
- "signature 2.0.0",
+ "signature 2.1.0",
+ "spki 0.7.2",
]
[[package]]
@@ -1543,12 +1498,12 @@ dependencies = [
[[package]]
name = "ed25519"
-version = "2.2.0"
+version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be522bee13fa6d8059f4903a4084aa3bd50725e18150202f0238deb615cd6371"
+checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963"
dependencies = [
- "pkcs8 0.10.1",
- "signature 2.0.0",
+ "pkcs8 0.10.2",
+ "signature 2.1.0",
]
[[package]]
@@ -1568,22 +1523,22 @@ dependencies = [
[[package]]
name = "ed25519-dalek"
-version = "2.0.0-rc.2"
+version = "2.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a"
+checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
dependencies = [
- "curve25519-dalek 4.0.0-rc.2",
- "ed25519 2.2.0",
+ "curve25519-dalek 4.0.0-rc.3",
+ "ed25519 2.2.1",
"serde",
- "sha2 0.10.6",
+ "sha2 0.10.8",
"zeroize",
]
[[package]]
name = "educe"
-version = "0.4.20"
+version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb0188e3c3ba8df5753894d54461f0e39bc91741dc5b22e1c46999ec2c71f4e4"
+checksum = "079044df30bb07de7d846d41a184c4b00e66ebdac93ee459253474f3a47e50ae"
dependencies = [
"enum-ordinalize",
"proc-macro2",
@@ -1593,9 +1548,9 @@ dependencies = [
[[package]]
name = "either"
-version = "1.8.1"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "elliptic-curve"
@@ -1606,7 +1561,7 @@ dependencies = [
"base16ct 0.1.1",
"crypto-bigint 0.4.9",
"der 0.6.1",
- "digest 0.10.6",
+ "digest 0.10.7",
"ff 0.12.1",
"generic-array",
"group 0.12.1",
@@ -1618,21 +1573,21 @@ dependencies = [
[[package]]
name = "elliptic-curve"
-version = "0.13.2"
+version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ea5a92946e8614bb585254898bb7dd1ddad241ace60c52149e3765e34cc039d"
+checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b"
dependencies = [
"base16ct 0.2.0",
- "crypto-bigint 0.5.1",
- "digest 0.10.6",
+ "crypto-bigint 0.5.2",
+ "digest 0.10.7",
"ff 0.13.0",
"generic-array",
"group 0.13.0",
"hkdf",
"pem-rfc7468 0.7.0",
- "pkcs8 0.10.1",
+ "pkcs8 0.10.2",
"rand_core 0.6.4",
- "sec1 0.7.1",
+ "sec1 0.7.3",
"subtle",
"zeroize",
]
@@ -1640,7 +1595,7 @@ dependencies = [
[[package]]
name = "email"
version = "0.0.21"
-source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e"
+source = "git+https://github.com/deltachat/rust-email?branch=master#37778c89d5eb5a94b7983f3f37ff67769bde3cf9"
dependencies = [
"base64 0.11.0",
"chrono",
@@ -1753,41 +1708,27 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "enum-as-inner"
-version = "0.5.1"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
+checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
]
[[package]]
name = "enum-ordinalize"
-version = "3.1.12"
+version = "3.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a62bb1df8b45ecb7ffa78dca1c17a438fb193eb083db0b1b494d2a61bcb5096a"
+checksum = "e4f76552f53cefc9a7f64987c3701b99d982f7690606fd67de1d09712fbf52f1"
dependencies = [
"num-bigint",
"num-traits",
"proc-macro2",
"quote",
- "rustc_version",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "env_logger"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
-dependencies = [
- "atty",
- "humantime 1.3.0",
- "log",
- "regex",
- "termcolor",
+ "syn 2.0.29",
]
[[package]]
@@ -1796,7 +1737,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
dependencies = [
- "humantime 2.1.0",
+ "humantime",
"is-terminal",
"log",
"regex",
@@ -1804,14 +1745,20 @@ dependencies = [
]
[[package]]
-name = "errno"
-version = "0.3.0"
+name = "equivalent"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
dependencies = [
"errno-dragonfly",
"libc",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -1893,14 +1840,29 @@ dependencies = [
]
[[package]]
-name = "fd-lock"
-version = "3.0.11"
+name = "fastrand"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9799aefb4a2e4a01cc47610b1dd47c18ab13d991f27bbcaed9296f5a53d5cbad"
+checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
+
+[[package]]
+name = "fd-lock"
+version = "3.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [
"cfg-if",
"rustix",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "fdeflate"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10"
+dependencies = [
+ "simd-adler32",
]
[[package]]
@@ -1925,27 +1887,27 @@ dependencies = [
[[package]]
name = "fiat-crypto"
-version = "0.1.19"
+version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93ace6ec7cc19c8ed33a32eaa9ea692d7faea05006b5356b9e2b668ec4bc3955"
+checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
[[package]]
name = "filetime"
-version = "0.2.20"
+version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412"
+checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.2.16",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
name = "flate2"
-version = "1.0.25"
+version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1987,9 +1949,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
-version = "1.1.0"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
dependencies = [
"percent-encoding",
]
@@ -2048,11 +2010,11 @@ checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
[[package]]
name = "futures-lite"
-version = "1.12.0"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
- "fastrand",
+ "fastrand 1.9.0",
"futures-core",
"futures-io",
"memchr",
@@ -2069,7 +2031,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.13",
+ "syn 2.0.29",
]
[[package]]
@@ -2104,9 +2066,9 @@ dependencies = [
[[package]]
name = "generic-array"
-version = "0.14.6"
+version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
@@ -2126,9 +2088,9 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.8"
+version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
@@ -2149,9 +2111,9 @@ dependencies = [
[[package]]
name = "gimli"
-version = "0.27.2"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "group"
@@ -2177,9 +2139,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.3.17"
+version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f"
+checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
dependencies = [
"bytes",
"fnv",
@@ -2187,7 +2149,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
- "indexmap",
+ "indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@@ -2205,17 +2167,24 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
dependencies = [
"ahash",
+ "allocator-api2",
]
[[package]]
name = "hashlink"
-version = "0.8.1"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
+checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f"
dependencies = [
- "hashbrown",
+ "hashbrown 0.14.2",
]
[[package]]
@@ -2226,27 +2195,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
-version = "0.1.19"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "hermit-abi"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "hermit-abi"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
@@ -2254,6 +2205,51 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+[[package]]
+name = "hickory-proto"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf"
+dependencies = [
+ "async-trait",
+ "cfg-if",
+ "data-encoding",
+ "enum-as-inner",
+ "futures-channel",
+ "futures-io",
+ "futures-util",
+ "idna",
+ "ipnet",
+ "once_cell",
+ "rand 0.8.5",
+ "thiserror",
+ "tinyvec",
+ "tokio",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "hickory-resolver"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35b8f021164e6a984c9030023544c57789c51760065cd510572fedcfb04164e8"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "hickory-proto",
+ "ipconfig",
+ "lru-cache",
+ "once_cell",
+ "parking_lot",
+ "rand 0.8.5",
+ "resolv-conf",
+ "smallvec",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
[[package]]
name = "hkdf"
version = "0.12.3"
@@ -2269,7 +2265,16 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
- "digest 0.10.6",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "home"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
+dependencies = [
+ "windows-sys 0.48.0",
]
[[package]]
@@ -2319,9 +2324,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "human-panic"
-version = "1.1.3"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a6557b29bbdc9d6c7a5cdbe2962e78eaf48115e8d55b0b62282956981c1f605"
+checksum = "38a841f87949b0dd751864e769a870be79dc34abcee1cf31d737a61d498b22b6"
dependencies = [
"backtrace",
"os_info",
@@ -2337,16 +2342,7 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
- "libm 0.2.6",
-]
-
-[[package]]
-name = "humantime"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
-dependencies = [
- "quick-error 1.2.3",
+ "libm",
]
[[package]]
@@ -2357,9 +2353,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
-version = "0.14.24"
+version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
"bytes",
"futures-channel",
@@ -2372,7 +2368,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
- "socket2",
+ "socket2 0.4.9",
"tokio",
"tower-service",
"tracing",
@@ -2394,26 +2390,25 @@ dependencies = [
[[package]]
name = "iana-time-zone"
-version = "0.1.53"
+version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
- "winapi",
+ "windows 0.48.0",
]
[[package]]
name = "iana-time-zone-haiku"
-version = "0.1.1"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
- "cxx",
- "cxx-build",
+ "cc",
]
[[package]]
@@ -2433,20 +2428,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
-version = "0.2.3"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
-dependencies = [
- "matches",
- "unicode-bidi",
- "unicode-normalization",
-]
-
-[[package]]
-name = "idna"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@@ -2454,9 +2438,9 @@ dependencies = [
[[package]]
name = "image"
-version = "0.24.6"
+version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
+checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
dependencies = [
"bytemuck",
"byteorder",
@@ -2479,12 +2463,22 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "1.9.2"
+version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
- "hashbrown",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.2",
]
[[package]]
@@ -2505,33 +2499,23 @@ dependencies = [
"cfg-if",
]
-[[package]]
-name = "io-lifetimes"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
-dependencies = [
- "libc",
- "windows-sys 0.45.0",
-]
-
[[package]]
name = "ipconfig"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be"
+checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
- "socket2",
+ "socket2 0.5.3",
"widestring",
- "winapi",
+ "windows-sys 0.48.0",
"winreg",
]
[[package]]
name = "ipnet"
-version = "2.7.1"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
+checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "iroh"
@@ -2541,7 +2525,7 @@ checksum = "e4fb9858c8cd3dd924a5da5bc511363845a9bcfdfac066bb2ef8454eb6111546"
dependencies = [
"abao",
"anyhow",
- "base64 0.21.0",
+ "base64 0.21.3",
"blake3",
"bytes",
"default-net",
@@ -2579,14 +2563,13 @@ dependencies = [
[[package]]
name = "is-terminal"
-version = "0.4.6"
+version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8"
+checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
- "hermit-abi 0.3.1",
- "io-lifetimes",
+ "hermit-abi",
"rustix",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -2600,9 +2583,9 @@ dependencies = [
[[package]]
name = "itoa"
-version = "1.0.5"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jpeg-decoder"
@@ -2612,9 +2595,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
[[package]]
name = "js-sys"
-version = "0.3.61"
+version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
dependencies = [
"wasm-bindgen",
]
@@ -2630,9 +2613,9 @@ dependencies = [
[[package]]
name = "keccak"
-version = "0.1.3"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768"
+checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940"
dependencies = [
"cpufeatures",
]
@@ -2672,21 +2655,15 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.140"
+version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libm"
-version = "0.1.4"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a"
-
-[[package]]
-name = "libm"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
+checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
[[package]]
name = "libsqlite3-sys"
@@ -2700,15 +2677,6 @@ dependencies = [
"vcpkg",
]
-[[package]]
-name = "link-cplusplus"
-version = "1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
-dependencies = [
- "cc",
-]
-
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -2717,15 +2685,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
-version = "0.3.1"
+version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
+checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "lock_api"
-version = "0.4.9"
+version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
dependencies = [
"autocfg",
"scopeguard",
@@ -2733,12 +2701,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.17"
+version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
-dependencies = [
- "cfg-if",
-]
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "lru-cache"
@@ -2772,20 +2737,14 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
- "regex-automata",
+ "regex-automata 0.1.10",
]
-[[package]]
-name = "matches"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
-
[[package]]
name = "matchit"
-version = "0.7.0"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
+checksum = "67827e6ea8ee8a7c4a72227ef4fc08957040acffdb5f122733b24fa12daff41b"
[[package]]
name = "md-5"
@@ -2793,7 +2752,7 @@ version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca"
dependencies = [
- "digest 0.10.6",
+ "digest 0.10.7",
]
[[package]]
@@ -2804,24 +2763,24 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1"
[[package]]
name = "memchr"
-version = "2.5.0"
+version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "memoffset"
-version = "0.8.0"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
-version = "0.3.16"
+version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
@@ -2831,23 +2790,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
-version = "0.6.2"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
+ "simd-adler32",
]
[[package]]
name = "mio"
-version = "0.8.6"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
+checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
- "log",
"wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -2862,7 +2821,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
- "getrandom 0.2.8",
+ "getrandom 0.2.10",
]
[[package]]
@@ -2965,9 +2924,9 @@ dependencies = [
[[package]]
name = "ntapi"
-version = "0.4.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
+checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
@@ -2995,13 +2954,13 @@ dependencies = [
[[package]]
name = "num-bigint-dig"
-version = "0.8.2"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
dependencies = [
"byteorder",
"lazy_static",
- "libm 0.2.6",
+ "libm",
"num-integer",
"num-iter",
"num-traits",
@@ -3013,13 +2972,13 @@ dependencies = [
[[package]]
name = "num-derive"
-version = "0.3.3"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
]
[[package]]
@@ -3056,29 +3015,29 @@ dependencies = [
[[package]]
name = "num-traits"
-version = "0.2.15"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg",
- "libm 0.2.6",
+ "libm",
]
[[package]]
name = "num_cpus"
-version = "1.15.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
- "hermit-abi 0.2.6",
+ "hermit-abi",
"libc",
]
[[package]]
name = "object"
-version = "0.30.3"
+version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439"
+checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
dependencies = [
"memchr",
]
@@ -3094,9 +3053,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.17.1"
+version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "oorandom"
@@ -3112,11 +3071,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
-version = "0.10.48"
+version = "0.10.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2"
+checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.3.3",
"cfg-if",
"foreign-types",
"libc",
@@ -3127,13 +3086,13 @@ dependencies = [
[[package]]
name = "openssl-macros"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
]
[[package]]
@@ -3144,20 +3103,19 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
-version = "111.25.1+1.1.1t"
+version = "300.1.5+3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ef9a9cc6ea7d9d5e7c4a913dc4b48d0e359eddf01af1dfec96ba7064b4aba10"
+checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
-version = "0.9.83"
+version = "0.9.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b"
+checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
dependencies = [
- "autocfg",
"cc",
"libc",
"openssl-src",
@@ -3166,45 +3124,22 @@ dependencies = [
]
[[package]]
-name = "os_info"
-version = "3.6.0"
+name = "option-ext"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c424bc68d15e0778838ac013b5b3449544d8133633d8016319e7e05a820b8c0"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "os_info"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
dependencies = [
"log",
"serde",
"winapi",
]
-[[package]]
-name = "os_str_bytes"
-version = "6.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
-
-[[package]]
-name = "ouroboros"
-version = "0.15.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db"
-dependencies = [
- "aliasable",
- "ouroboros_macro",
-]
-
-[[package]]
-name = "ouroboros_macro"
-version = "0.15.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7"
-dependencies = [
- "Inflector",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
[[package]]
name = "overload"
version = "0.1.1"
@@ -3219,19 +3154,19 @@ checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
dependencies = [
"ecdsa 0.14.8",
"elliptic-curve 0.12.3",
- "sha2 0.10.6",
+ "sha2 0.10.8",
]
[[package]]
name = "p256"
-version = "0.13.0"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7270da3e5caa82afd3deb054cc237905853813aea3859544bc082c3fe55b8d47"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
- "ecdsa 0.16.2",
- "elliptic-curve 0.13.2",
+ "ecdsa 0.16.8",
+ "elliptic-curve 0.13.5",
"primeorder",
- "sha2 0.10.6",
+ "sha2 0.10.8",
]
[[package]]
@@ -3242,7 +3177,7 @@ checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa"
dependencies = [
"ecdsa 0.14.8",
"elliptic-curve 0.12.3",
- "sha2 0.10.6",
+ "sha2 0.10.8",
]
[[package]]
@@ -3251,27 +3186,17 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
dependencies = [
- "ecdsa 0.16.2",
- "elliptic-curve 0.13.2",
+ "ecdsa 0.16.8",
+ "elliptic-curve 0.13.5",
"primeorder",
- "sha2 0.10.6",
-]
-
-[[package]]
-name = "packed_simd_2"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282"
-dependencies = [
- "cfg-if",
- "libm 0.1.4",
+ "sha2 0.10.8",
]
[[package]]
name = "parking"
-version = "2.0.0"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
[[package]]
name = "parking_lot"
@@ -3285,22 +3210,22 @@ dependencies = [
[[package]]
name = "parking_lot_core"
-version = "0.9.7"
+version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall 0.2.16",
+ "redox_syscall 0.3.5",
"smallvec",
- "windows-sys 0.45.0",
+ "windows-targets 0.48.1",
]
[[package]]
name = "paste"
-version = "1.0.12"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pem"
@@ -3331,18 +3256,18 @@ dependencies = [
[[package]]
name = "percent-encoding"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pgp"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37a79d6411154d1a9908e7a2c4bac60a5742f6125823c2c30780c7039aef02f0"
+checksum = "27e1f8e085bfa9b85763fe3ddaacbe90a09cd847b3833129153a6cb063bbe132"
dependencies = [
"aes",
- "base64 0.21.0",
+ "base64 0.21.3",
"bitfield",
"block-padding",
"blowfish",
@@ -3355,11 +3280,12 @@ dependencies = [
"chrono",
"cipher",
"crc24",
+ "curve25519-dalek 4.0.0-rc.3",
"derive_builder",
"des",
- "digest 0.10.6",
- "ed25519-dalek 2.0.0-rc.2",
- "elliptic-curve 0.13.2",
+ "digest 0.10.7",
+ "ed25519-dalek 2.0.0-rc.3",
+ "elliptic-curve 0.13.5",
"flate2",
"generic-array",
"hex",
@@ -3370,15 +3296,15 @@ dependencies = [
"num-bigint-dig",
"num-derive",
"num-traits",
- "p256 0.13.0",
+ "p256 0.13.2",
"p384 0.13.0",
"rand 0.8.5",
"ripemd",
- "rsa 0.9.0-pre.0",
+ "rsa 0.9.2",
"sha1",
- "sha2 0.10.6",
+ "sha2 0.10.8",
"sha3",
- "signature 2.0.0",
+ "signature 2.1.0",
"smallvec",
"thiserror",
"twofish",
@@ -3388,29 +3314,29 @@ dependencies = [
[[package]]
name = "pin-project"
-version = "1.0.12"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
-version = "1.0.12"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
]
[[package]]
name = "pin-project-lite"
-version = "0.2.9"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@@ -3432,14 +3358,13 @@ dependencies = [
[[package]]
name = "pkcs1"
-version = "0.7.1"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "178ba28ece1961eafdff1991bd1744c29564cbab5d803f3ccb4a4895a6c550a7"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
- "der 0.7.1",
- "pkcs8 0.10.1",
- "spki 0.7.0",
- "zeroize",
+ "der 0.7.7",
+ "pkcs8 0.10.2",
+ "spki 0.7.2",
]
[[package]]
@@ -3454,19 +3379,19 @@ dependencies = [
[[package]]
name = "pkcs8"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d2820d87d2b008616e5c27212dd9e0e694fb4c6b522de06094106813328cb49"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
- "der 0.7.1",
- "spki 0.7.0",
+ "der 0.7.7",
+ "spki 0.7.2",
]
[[package]]
name = "pkg-config"
-version = "0.3.26"
+version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "platforms"
@@ -3476,9 +3401,9 @@ checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630"
[[package]]
name = "plotters"
-version = "0.3.4"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97"
+checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
dependencies = [
"num-traits",
"plotters-backend",
@@ -3489,42 +3414,43 @@ dependencies = [
[[package]]
name = "plotters-backend"
-version = "0.3.4"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142"
+checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609"
[[package]]
name = "plotters-svg"
-version = "0.3.3"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f"
+checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab"
dependencies = [
"plotters-backend",
]
[[package]]
name = "png"
-version = "0.17.7"
+version = "0.17.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
+checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
+ "fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "portable-atomic"
-version = "1.0.1"
+version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39c00c8683a03bd4fe7db7dd64ab4abee6b42166bc81231da983486ce96be51a"
+checksum = "f32154ba0af3a075eefa1eda8bb414ee928f62303a54ea85b8d6638ff1a6ee9e"
[[package]]
name = "postcard"
-version = "1.0.4"
+version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00"
+checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb"
dependencies = [
"cobs",
"const_format",
@@ -3550,22 +3476,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
-name = "pretty_env_logger"
-version = "0.4.0"
+name = "pretty_assertions"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
+checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
- "env_logger 0.7.1",
+ "diff",
+ "yansi",
+]
+
+[[package]]
+name = "pretty_env_logger"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
+dependencies = [
+ "env_logger",
"log",
]
[[package]]
name = "primeorder"
-version = "0.13.0"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7613fdcc0831c10060fa69833ea8fa2caa94b6456f51e25356a885b530a2e3d0"
+checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3"
dependencies = [
- "elliptic-curve 0.13.2",
+ "elliptic-curve 0.13.5",
]
[[package]]
@@ -3594,28 +3530,26 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.55"
+version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d0dd4be24fcdcfeaa12a432d588dc59bbad6cad3510c67e74a2b6b2fc950564"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "proptest"
-version = "1.1.0"
+version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70"
+checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e"
dependencies = [
- "bitflags 1.3.2",
- "byteorder",
+ "bitflags 2.3.3",
"lazy_static",
"num-traits",
- "quick-error 2.0.1",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_xorshift",
- "regex-syntax",
+ "regex-syntax 0.7.5",
"unarray",
]
@@ -3649,17 +3583,11 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-[[package]]
-name = "quick-error"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
-
[[package]]
name = "quick-xml"
-version = "0.28.1"
+version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5c1a97b1bc42b1d550bfb48d4262153fe400a12bab1511821736f7eac76d7e2"
+checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
"memchr",
]
@@ -3707,25 +3635,25 @@ source = "git+https://github.com/quinn-rs/quinn?branch=main#11b34a7b2652010cdbbd
dependencies = [
"libc",
"quinn-proto",
- "socket2",
+ "socket2 0.4.9",
"tracing",
"windows-sys 0.45.0",
]
[[package]]
name = "quote"
-version = "1.0.26"
+version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "quoted_printable"
-version = "0.4.7"
+version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6"
+checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
[[package]]
name = "radix_trie"
@@ -3796,7 +3724,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
- "getrandom 0.2.8",
+ "getrandom 0.2.10",
]
[[package]]
@@ -3823,9 +3751,9 @@ version = "1.0.0"
[[package]]
name = "rayon"
-version = "1.6.1"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
dependencies = [
"either",
"rayon-core",
@@ -3833,9 +3761,9 @@ dependencies = [
[[package]]
name = "rayon-core"
-version = "1.10.2"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
@@ -3851,7 +3779,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b"
dependencies = [
"pem",
"ring",
- "time 0.3.20",
+ "time 0.3.24",
"yasna",
]
@@ -3879,20 +3807,21 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
- "getrandom 0.2.8",
+ "getrandom 0.2.10",
"redox_syscall 0.2.16",
"thiserror",
]
[[package]]
name = "regex"
-version = "1.7.3"
+version = "1.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
+checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax",
+ "regex-automata 0.3.9",
+ "regex-syntax 0.7.5",
]
[[package]]
@@ -3901,7 +3830,18 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
- "regex-syntax",
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.7.5",
]
[[package]]
@@ -3911,12 +3851,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
-name = "reqwest"
-version = "0.11.16"
+name = "regex-syntax"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
+checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+
+[[package]]
+name = "reqwest"
+version = "0.11.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [
- "base64 0.21.0",
+ "base64 0.21.3",
"bytes",
"encoding_rs",
"futures-core",
@@ -3954,7 +3900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
dependencies = [
"hostname",
- "quick-error 1.2.3",
+ "quick-error",
]
[[package]]
@@ -3999,7 +3945,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
dependencies = [
- "digest 0.10.6",
+ "digest 0.10.7",
]
[[package]]
@@ -4009,7 +3955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c"
dependencies = [
"byteorder",
- "digest 0.10.6",
+ "digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-iter",
@@ -4025,20 +3971,22 @@ dependencies = [
[[package]]
name = "rsa"
-version = "0.9.0-pre.0"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7bc1d34159d63536b4d89944e9ab5bb952f45db6fa0b8b03c2f8c09fb5b7171"
+checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8"
dependencies = [
"byteorder",
- "digest 0.10.6",
+ "const-oid",
+ "digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-iter",
"num-traits",
- "pkcs1 0.7.1",
- "pkcs8 0.10.1",
+ "pkcs1 0.7.5",
+ "pkcs8 0.10.2",
"rand_core 0.6.4",
- "signature 2.0.0",
+ "signature 2.1.0",
+ "spki 0.7.2",
"subtle",
"zeroize",
]
@@ -4049,7 +3997,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
- "bitflags 2.0.2",
+ "bitflags 2.3.3",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@@ -4065,9 +4013,9 @@ checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b"
[[package]]
name = "rustc-demangle"
-version = "0.1.21"
+version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
@@ -4095,16 +4043,15 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.37.6"
+version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d097081ed288dfe45699b72f5b5d648e5f15d64d900c7080273baa20c16a6849"
+checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.3.3",
"errno",
- "io-lifetimes",
"libc",
"linux-raw-sys",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -4120,9 +4067,9 @@ dependencies = [
[[package]]
name = "rustls-native-certs"
-version = "0.6.2"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
@@ -4132,30 +4079,30 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
+checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [
- "base64 0.21.0",
+ "base64 0.21.3",
]
[[package]]
name = "rustversion"
-version = "1.0.11"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustyline"
-version = "11.0.0"
+version = "12.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfc8644681285d1fb67a467fb3021bfea306b99b4146b166a1fe3ada965eece"
+checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.3.3",
"cfg-if",
"clipboard-win",
- "dirs-next",
"fd-lock",
+ "home",
"libc",
"log",
"memchr",
@@ -4170,9 +4117,9 @@ dependencies = [
[[package]]
name = "ryu"
-version = "1.0.12"
+version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "safemem"
@@ -4191,9 +4138,9 @@ dependencies = [
[[package]]
name = "sanitize-filename"
-version = "0.4.0"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c"
+checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603"
dependencies = [
"lazy_static",
"regex",
@@ -4201,24 +4148,42 @@ dependencies = [
[[package]]
name = "schannel"
-version = "0.1.21"
+version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
- "windows-sys 0.42.0",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 1.0.109",
]
[[package]]
name = "scopeguard"
-version = "1.1.0"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-
-[[package]]
-name = "scratch"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
@@ -4246,23 +4211,23 @@ dependencies = [
[[package]]
name = "sec1"
-version = "0.7.1"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct 0.2.0",
- "der 0.7.1",
+ "der 0.7.7",
"generic-array",
- "pkcs8 0.10.1",
+ "pkcs8 0.10.2",
"subtle",
"zeroize",
]
[[package]]
name = "security-framework"
-version = "2.8.2"
+version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
@@ -4273,28 +4238,34 @@ dependencies = [
[[package]]
name = "security-framework-sys"
-version = "2.8.0"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
-name = "semver"
-version = "1.0.16"
+name = "self_cell"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
+checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
+
+[[package]]
+name = "semver"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
dependencies = [
"serde",
]
[[package]]
name = "serde"
-version = "1.0.159"
+version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
+checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
@@ -4310,29 +4281,40 @@ dependencies = [
[[package]]
name = "serde_bytes"
-version = "0.11.9"
+version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294"
+checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
-version = "1.0.159"
+version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
+checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.13",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
]
[[package]]
name = "serde_json"
-version = "1.0.95"
+version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
+checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
dependencies = [
"itoa",
"ryu",
@@ -4341,18 +4323,19 @@ dependencies = [
[[package]]
name = "serde_path_to_error"
-version = "0.1.9"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b04f22b563c91331a10074bda3dd5492e3cc39d56bd557e91c0af42b6c7341"
+checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
dependencies = [
+ "itoa",
"serde",
]
[[package]]
name = "serde_spanned"
-version = "0.6.1"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
+checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
@@ -4377,7 +4360,7 @@ checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
dependencies = [
"cfg-if",
"cpufeatures",
- "digest 0.10.6",
+ "digest 0.10.7",
]
[[package]]
@@ -4388,7 +4371,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [
"cfg-if",
"cpufeatures",
- "digest 0.10.6",
+ "digest 0.10.7",
]
[[package]]
@@ -4406,22 +4389,22 @@ dependencies = [
[[package]]
name = "sha2"
-version = "0.10.6"
+version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
- "digest 0.10.6",
+ "digest 0.10.7",
]
[[package]]
name = "sha3"
-version = "0.10.6"
+version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
- "digest 0.10.6",
+ "digest 0.10.7",
"keccak",
]
@@ -4449,20 +4432,26 @@ version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
dependencies = [
- "digest 0.10.6",
+ "digest 0.10.7",
"rand_core 0.6.4",
]
[[package]]
name = "signature"
-version = "2.0.0"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d"
+checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
dependencies = [
- "digest 0.10.6",
+ "digest 0.10.7",
"rand_core 0.6.4",
]
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
[[package]]
name = "slab"
version = "0.4.8"
@@ -4474,9 +4463,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.10.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
[[package]]
name = "smawk"
@@ -4494,6 +4483,16 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "socket2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "spin"
version = "0.5.2"
@@ -4521,12 +4520,12 @@ dependencies = [
[[package]]
name = "spki"
-version = "0.7.0"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0445c905640145c7ea8c1993555957f65e7c46d0535b91ba501bc9bfc85522f"
+checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
dependencies = [
"base64ct",
- "der 0.7.1",
+ "der 0.7.7",
]
[[package]]
@@ -4537,7 +4536,7 @@ checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1"
dependencies = [
"base64ct",
"pem-rfc7468 0.6.0",
- "sha2 0.10.6",
+ "sha2 0.10.8",
]
[[package]]
@@ -4552,18 +4551,12 @@ dependencies = [
"rand_core 0.6.4",
"rsa 0.7.2",
"sec1 0.3.0",
- "sha2 0.10.6",
+ "sha2 0.10.8",
"signature 1.6.4",
"ssh-encoding",
"zeroize",
]
-[[package]]
-name = "stable_deref_trait"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
-
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -4596,28 +4589,28 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
-version = "0.24.1"
+version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
[[package]]
name = "strum_macros"
-version = "0.24.3"
+version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
+checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
- "syn 1.0.109",
+ "syn 2.0.29",
]
[[package]]
name = "subtle"
-version = "2.4.1"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
@@ -4632,9 +4625,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.13"
+version = "2.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec"
+checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
dependencies = [
"proc-macro2",
"quote",
@@ -4675,9 +4668,9 @@ dependencies = [
[[package]]
name = "system-configuration"
-version = "0.5.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75182f12f490e953596550b65ee31bda7c8e043d9386174b353bda50838c3fd"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
@@ -4696,21 +4689,21 @@ dependencies = [
[[package]]
name = "tagger"
-version = "4.3.4"
+version = "4.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63"
+checksum = "094c9f64d6de9a8506b1e49b63a29333b37ed9e821ee04be694d431b3264c3c5"
[[package]]
name = "tempfile"
-version = "3.5.0"
+version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
+checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if",
- "fastrand",
+ "fastrand 2.0.0",
"redox_syscall 0.3.5",
"rustix",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -4724,9 +4717,9 @@ dependencies = [
[[package]]
name = "testdir"
-version = "0.7.3"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a45fc921e7c4ad1aedb3484811514f3e5cd187886e0bbf1302c175f7578ef552"
+checksum = "48b7965698cfb3d1ac1e6e54b4b45f5caa9e89bda223c8cf723d9cf53d7cefa7"
dependencies = [
"anyhow",
"backtrace",
@@ -4749,22 +4742,22 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.40"
+version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.40"
+version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.13",
+ "syn 2.0.29",
]
[[package]]
@@ -4790,10 +4783,11 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.20"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
+checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b"
dependencies = [
+ "deranged",
"itoa",
"serde",
"time-core",
@@ -4802,15 +4796,15 @@ dependencies = [
[[package]]
name = "time-core"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
-version = "0.2.8"
+version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
+checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd"
dependencies = [
"time-core",
]
@@ -4842,11 +4836,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.27.0"
+version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
+checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [
- "autocfg",
+ "backtrace",
"bytes",
"libc",
"mio",
@@ -4854,9 +4848,9 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2",
+ "socket2 0.5.3",
"tokio-macros",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -4871,13 +4865,13 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.0.0"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.13",
+ "syn 2.0.29",
]
[[package]]
@@ -4907,9 +4901,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
-version = "0.1.12"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -4918,14 +4912,14 @@ dependencies = [
[[package]]
name = "tokio-tar"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a50188549787c32c1c3d9c8c71ad7e003ccf2f102489c5a96e385c84760477f4"
+checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75"
dependencies = [
"filetime",
"futures-core",
"libc",
- "redox_syscall 0.2.16",
+ "redox_syscall 0.3.5",
"tokio",
"tokio-stream",
"xattr",
@@ -4933,9 +4927,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
-version = "0.18.0"
+version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
+checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
dependencies = [
"futures-util",
"log",
@@ -4945,9 +4939,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.7"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
+checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
dependencies = [
"bytes",
"futures-core",
@@ -4959,9 +4953,9 @@ dependencies = [
[[package]]
name = "toml"
-version = "0.7.3"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
+checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
dependencies = [
"serde",
"serde_spanned",
@@ -4971,20 +4965,20 @@ dependencies = [
[[package]]
name = "toml_datetime"
-version = "0.6.1"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
-version = "0.19.8"
+version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
+checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
- "indexmap",
+ "indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -5034,20 +5028,20 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.23"
+version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
]
[[package]]
name = "tracing-core"
-version = "0.1.30"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [
"once_cell",
"valuable",
@@ -5076,9 +5070,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.16"
+version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
+checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -5092,51 +5086,6 @@ dependencies = [
"tracing-log",
]
-[[package]]
-name = "trust-dns-proto"
-version = "0.22.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
-dependencies = [
- "async-trait",
- "cfg-if",
- "data-encoding",
- "enum-as-inner",
- "futures-channel",
- "futures-io",
- "futures-util",
- "idna 0.2.3",
- "ipnet",
- "lazy_static",
- "rand 0.8.5",
- "smallvec",
- "thiserror",
- "tinyvec",
- "tokio",
- "tracing",
- "url",
-]
-
-[[package]]
-name = "trust-dns-resolver"
-version = "0.22.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
-dependencies = [
- "cfg-if",
- "futures-util",
- "ipconfig",
- "lazy_static",
- "lru-cache",
- "parking_lot",
- "resolv-conf",
- "smallvec",
- "thiserror",
- "tokio",
- "tracing",
- "trust-dns-proto",
-]
-
[[package]]
name = "try-lock"
version = "0.2.4"
@@ -5145,13 +5094,13 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
-version = "0.18.0"
+version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
+checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
dependencies = [
- "base64 0.13.1",
"byteorder",
"bytes",
+ "data-encoding",
"http",
"httparse",
"log",
@@ -5179,9 +5128,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typescript-type-def"
-version = "0.5.6"
+version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e6b74ffbd5684d318252bb7182051df8c4ecc098b542f63fddf792e7f42aa02"
+checksum = "356e00027bd9ef773605a353070dc87684b25561a59087ea3ee3dd5fe8854e83"
dependencies = [
"serde_json",
"typescript-type-def-derive",
@@ -5189,9 +5138,9 @@ dependencies = [
[[package]]
name = "typescript-type-def-derive"
-version = "0.5.6"
+version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b10a4f5dd87c279f90beef31edb7055bfd1ceb66e73148de107a5c9005e9f864"
+checksum = "c4e696c28431595138cc53892104528152cbcf26653ae0aa655e4eaede5b9f69"
dependencies = [
"darling 0.13.4",
"ident_case",
@@ -5209,25 +5158,21 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-bidi"
-version = "0.3.10"
+version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
-version = "1.0.6"
+version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unicode-linebreak"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
-dependencies = [
- "hashbrown",
- "regex",
-]
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-normalization"
@@ -5264,12 +5209,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
-version = "2.3.1"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [
"form_urlencoded",
- "idna 0.3.0",
+ "idna",
"percent-encoding",
]
@@ -5281,17 +5226,17 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
-version = "1.3.0"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
+checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
- "getrandom 0.2.8",
+ "getrandom 0.2.10",
"serde",
]
@@ -5331,11 +5276,10 @@ dependencies = [
[[package]]
name = "want"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
- "log",
"try-lock",
]
@@ -5359,9 +5303,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
-version = "0.2.84"
+version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -5369,24 +5313,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.84"
+version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.34"
+version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
dependencies = [
"cfg-if",
"js-sys",
@@ -5396,9 +5340,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.84"
+version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -5406,28 +5350,28 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.84"
+version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.29",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.84"
+version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "web-sys"
-version = "0.3.61"
+version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -5435,9 +5379,9 @@ dependencies = [
[[package]]
name = "webpki"
-version = "0.22.0"
+version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
dependencies = [
"ring",
"untrusted",
@@ -5451,9 +5395,9 @@ checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "whoami"
-version = "1.3.0"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3"
+checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50"
dependencies = [
"wasm-bindgen",
"web-sys",
@@ -5461,9 +5405,9 @@ dependencies = [
[[package]]
name = "widestring"
-version = "0.5.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
+checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8"
[[package]]
name = "winapi"
@@ -5510,18 +5454,12 @@ dependencies = [
]
[[package]]
-name = "windows-sys"
-version = "0.42.0"
+name = "windows"
+version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc 0.42.1",
- "windows_i686_gnu 0.42.1",
- "windows_i686_msvc 0.42.1",
- "windows_x86_64_gnu 0.42.1",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc 0.42.1",
+ "windows-targets 0.48.1",
]
[[package]]
@@ -5530,7 +5468,16 @@ version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
- "windows-targets",
+ "windows-targets 0.42.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.1",
]
[[package]]
@@ -5539,20 +5486,41 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc 0.42.1",
- "windows_i686_gnu 0.42.1",
- "windows_i686_msvc 0.42.1",
- "windows_x86_64_gnu 0.42.1",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc 0.42.1",
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
@@ -5562,9 +5530,15 @@ checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
@@ -5574,9 +5548,15 @@ checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
@@ -5586,9 +5566,15 @@ checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
@@ -5598,15 +5584,27 @@ checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
@@ -5616,36 +5614,44 @@ checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
-version = "0.4.1"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
+checksum = "f46aab759304e4d7b2075a9aecba26228bb073ee8c50db796b2c72c676b5d807"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
-version = "0.10.1"
+version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
- "winapi",
+ "cfg-if",
+ "windows-sys 0.48.0",
]
[[package]]
name = "x25519-dalek"
-version = "2.0.0-pre.1"
+version = "2.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df"
+checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
dependencies = [
- "curve25519-dalek 3.2.0",
+ "curve25519-dalek 4.0.0-rc.3",
"rand_core 0.6.4",
+ "serde",
"zeroize",
]
@@ -5664,32 +5670,38 @@ dependencies = [
"oid-registry",
"rusticata-macros",
"thiserror",
- "time 0.3.20",
+ "time 0.3.24",
]
[[package]]
name = "xattr"
-version = "0.2.3"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
+checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985"
dependencies = [
"libc",
]
[[package]]
-name = "yasna"
+name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aed2e7a52e3744ab4d0c05c20aa065258e84c49fd4226f5191b2ed29712710b4"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
- "time 0.3.20",
+ "time 0.3.24",
]
[[package]]
name = "yerpc"
-version = "0.4.3"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6a0257f42e6bdc187f37074723b6094da3502cee21ae517b3c54d2c37d506e7"
+checksum = "75b5547af776328f66a5476ea3b7c0789e6fed164eb32d1a2122cfb39ffa505d"
dependencies = [
"anyhow",
"async-channel",
@@ -5699,6 +5711,7 @@ dependencies = [
"futures",
"futures-util",
"log",
+ "schemars",
"serde",
"serde_json",
"tokio",
@@ -5709,34 +5722,53 @@ dependencies = [
[[package]]
name = "yerpc_derive"
-version = "0.4.3"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bd53ff9053698697b92c2535bf7ecb983fd5d546d690b7c725e5070d6d9a620"
+checksum = "f321bb5f728fb066af06c5a994e4375f1f8b054ee6d650766f0bd68dfa4faefe"
dependencies = [
"convert_case 0.5.0",
- "darling 0.14.3",
+ "darling 0.14.4",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
-name = "zeroize"
-version = "1.5.7"
+name = "zerocopy"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"
+checksum = "7a7af71d8643341260a65f89fa60c0eeaa907f34544d8f6d9b0df72f069b5e74"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9731702e2f0617ad526794ae28fbc6f6ca8849b5ba729666c2a5bc4b6ddee2cd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
-version = "1.3.3"
+version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
- "synstructure",
+ "syn 2.0.29",
]
diff --git a/Cargo.toml b/Cargo.toml
index 281aeeb41..e71720265 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,9 +1,9 @@
[package]
name = "deltachat"
-version = "1.112.6"
+version = "1.126.1"
edition = "2021"
license = "MPL-2.0"
-rust-version = "1.65"
+rust-version = "1.67"
[profile.dev]
debug = 0
@@ -23,6 +23,8 @@ opt-level = "z"
lto = true
panic = 'abort'
opt-level = "z"
+codegen-units = 1
+strip = true
[patch.crates-io]
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
@@ -35,74 +37,77 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "1.8.0"
-async-imap = { version = "0.7.0", default-features = false, features = ["runtime-tokio"] }
+async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = "0.21"
-brotli = "3.3"
+brotli = { version = "3.4", default-features=false, features = ["std"] }
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.8"
+fd-lock = "3.0.11"
futures = "0.3"
-futures-lite = "1.12.0"
+futures-lite = "1.13.0"
hex = "0.4.0"
+hickory-resolver = "0.24"
humansize = "2"
-image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
+image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.1", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
mailparse = "0.14"
-num_cpus = "1.15"
-num-derive = "0.3"
+mime = "0.3.17"
+num_cpus = "1.16"
+num-derive = "0.4"
num-traits = "0.2"
-once_cell = "1.17.0"
-percent-encoding = "2.2"
+once_cell = "1.18.0"
+percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
-pretty_env_logger = { version = "0.4", optional = true }
+pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
-quick-xml = "0.28"
+quick-xml = "0.30"
rand = "0.8"
-regex = "1.7"
-reqwest = { version = "0.11.16", features = ["json"] }
+regex = "1.9"
+reqwest = { version = "0.11.20", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rust-hsluv = "0.1"
-sanitize-filename = "0.4"
+sanitize-filename = "0.5"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
-strum = "0.24"
-strum_macros = "0.24"
+strum = "0.25"
+strum_macros = "0.25"
tagger = "4.3.4"
textwrap = "0.16.0"
thiserror = "1"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
-tokio-stream = { version = "0.1.11", features = ["fs"] }
+tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
-tokio-util = "0.7.7"
+tokio-util = "0.7.9"
toml = "0.7"
-trust-dns-resolver = "0.22"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies]
ansi_term = "0.12.0"
-criterion = { version = "0.4.0", features = ["async_tokio"] }
-futures-lite = "1.12"
+criterion = { version = "0.5.1", features = ["async_tokio"] }
+futures-lite = "1.13"
log = "0.4"
-pretty_env_logger = "0.4"
+pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
-testdir = "0.7.3"
+testdir = "0.8.0"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
+pretty_assertions = "1.3.0"
[workspace]
members = [
@@ -115,11 +120,6 @@ members = [
"format-flowed",
]
-[[example]]
-name = "simple"
-path = "examples/simple.rs"
-
-
[[bench]]
name = "create_account"
harness = false
diff --git a/LICENSE b/LICENSE
index c7a27cda5..7d0aa6a5c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
diff --git a/README.md b/README.md
index 937967a1c..96ff5b485 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,16 @@
-# Delta Chat Rust
+
+
+
-> Deltachat-core written in Rust
+
+
+
+
+
-[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
+
+The core library for Delta Chat, written in Rust
+
## Installing Rust and Cargo
@@ -167,8 +175,8 @@ Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js**
- - over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- - over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
+ - 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/)\]
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 000000000..b9f50e9ca
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,21 @@
+# Releasing a new version of DeltaChat core
+
+For example, to release version 1.116.0 of the core, do the following steps.
+
+1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
+
+2. Run `npm run build:core:constants` in the root of the repository
+ and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
+
+3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
+
+4. Update the version by running `scripts/set_core_version.py 1.116.0`.
+
+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 -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 -n ''`.
diff --git a/benches/create_account.rs b/benches/create_account.rs
index 5e1ae8561..c487004ac 100644
--- a/benches/create_account.rs
+++ b/benches/create_account.rs
@@ -8,7 +8,8 @@ async fn create_accounts(n: u32) {
let dir = tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
- let mut accounts = Accounts::new(p.clone()).await.unwrap();
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
for expected_id in 2..n {
let id = accounts.add_account().await.unwrap();
diff --git a/cliff.toml b/cliff.toml
new file mode 100644
index 000000000..98a959c28
--- /dev/null
+++ b/cliff.toml
@@ -0,0 +1,79 @@
+# configuration file for git-cliff
+# see https://git-cliff.org/docs/configuration/
+
+
+[git]
+# parse the commits based on https://www.conventionalcommits.org
+conventional_commits = true
+# filter out the commits that are not conventional
+filter_unconventional = false
+# process each line of a commit as an individual commit
+split_commits = false
+# regex for preprocessing the commit messages
+commit_preprocessors = [
+ { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers
+]
+# regex for parsing and grouping commits
+commit_parsers = [
+ { message = "^feat", group = "Features / Changes"},
+ { message = "^fix", group = "Fixes"},
+ { message = "^api", group = "API-Changes" },
+ { message = "^refactor", group = "Refactor"},
+ { message = "^perf", group = "Performance"},
+ { message = "^test", group = "Tests"},
+ { message = "^style", group = "Styling"},
+ { message = "^chore\\(release\\): prepare for", skip = true},
+ { message = "^chore", group = "Miscellaneous Tasks"},
+ { message = "^build", group = "Build system"},
+ { message = "^docs", group = "Documentation"},
+ { message = "^ci", group = "CI"},
+ { message = ".*", group = "Other"},
+# { body = ".*security", group = "Security"},
+]
+# protect breaking changes from being skipped due to matching a skipping commit_parser
+protect_breaking_commits = true
+# filter out the commits that are not matched by commit parsers
+filter_commits = true
+# glob pattern for matching git tags
+tag_pattern = "v[0-9]*"
+# regex for skipping tags
+#skip_tags = "v0.1.0-beta.1"
+# regex for ignoring tags
+ignore_tags = ""
+# sort the tags topologically
+topo_order = false
+# sort the commits inside sections by oldest/newest order
+sort_commits = "oldest"
+# limit the number of commits included in the changelog.
+# limit_commits = 42
+
+
+[changelog]
+# changelog header
+header = """
+# Changelog\n
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#templates
+body = """
+{% if version %}\
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+ ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ### {{ group | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.breaking %}[**breaking**] {% endif %}\
+ {% if commit.scope %}{{ commit.scope }}: {% endif %}\
+ {{ commit.message | upper_first }}.\
+ {% if commit.footers is defined %}\
+ {% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
+ {% raw %} {% endraw %}- {{ footer.value }}\
+ {% endif %}{% endfor %}\
+ {% endif%}\
+ {% endfor %}
+{% endfor %}\n
+"""
+# remove the leading and trailing whitespace from the template
+trim = true
diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml
index f6ed211e4..fe8064f85 100644
--- a/deltachat-ffi/Cargo.toml
+++ b/deltachat-ffi/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
-version = "1.112.6"
+version = "1.126.1"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -24,7 +24,8 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.8"
-once_cell = "1.17.0"
+once_cell = "1.18.0"
+yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
[features]
default = ["vendored"]
diff --git a/deltachat-ffi/Doxyfile b/deltachat-ffi/Doxyfile
index 03365989d..5c60f15e3 100644
--- a/deltachat-ffi/Doxyfile
+++ b/deltachat-ffi/Doxyfile
@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
# exclude all test directories use the pattern */test/*
######################################################
-EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
+EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
EXCLUDE_SYMBOLS += _dc_* jsmn*
######################################################
diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h
index c76a5891a..ab4d746d4 100644
--- a/deltachat-ffi/deltachat.h
+++ b/deltachat-ffi/deltachat.h
@@ -25,6 +25,7 @@ typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
typedef struct _dc_backup_provider dc_backup_provider_t;
+typedef struct _dc_http_response dc_http_response_t;
// Alias for backwards compatibility, use dc_event_emitter_t instead.
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
@@ -300,6 +301,19 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
int dc_context_open (dc_context_t *context, const char* passphrase);
+/**
+ * Changes the passphrase on the open database.
+ * Existing database must already be encrypted and the passphrase cannot be NULL or empty.
+ * It is impossible to encrypt unencrypted database with this method and vice versa.
+ *
+ * @memberof dc_context_t
+ * @param context The context object.
+ * @param passphrase The new passphrase.
+ * @return 1 on success, 0 on error.
+ */
+int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
+
+
/**
* Returns 1 if database is open.
*
@@ -370,7 +384,12 @@ char* dc_get_blobdir (const dc_context_t* context);
/**
* Configure the context. The configuration is handled by key=value pairs as:
*
- * - `addr` = address to display (always needed)
+ * - `addr` = Email address to use for configuration.
+ * If dc_configure() fails this is not the email address actually in use.
+ * Use `configured_addr` to find out the email address actually in use.
+ * - `configured_addr` = Email address actually in use.
+ * Unless for testing, do not set this value using dc_set_config().
+ * Instead, set `addr` and call dc_configure().
* - `mail_server` = IMAP-server, guessed if left out
* - `mail_user` = IMAP-username, guessed if left out
* - `mail_pw` = IMAP-password (always needed)
@@ -419,17 +438,19 @@ char* dc_get_blobdir (const dc_context_t* context);
* 0=watch all folders normally (default)
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
- * show direct replies to chats only (default),
+ * show direct replies to chats only,
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
- * also show mails of unconfirmed contacts.
+ * also show mails of unconfirmed contacts (default).
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
- * generate Ed25519 keypair
+ * generate Curve25519 keypair
+ * DC_KEY_GEN_RSA4096 (3)=
+ * generate RSA 4096 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
@@ -460,8 +481,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
- * adds Auto-Submitted header to outgoing messages
- * and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
+ * adds Auto-Submitted header to outgoing messages,
+ * accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
+ * and does not cut large incoming text messages.
* - `last_msg_id` = database ID of the last message processed by the bot.
* This ID and IDs below it are guaranteed not to be returned
* by dc_get_next_msgs() and dc_wait_next_msgs().
@@ -475,6 +497,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
+ * - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
+ * 0=use IMAP IDLE if the server supports it.
+ * This is a developer option used for testing polling used as an IDLE fallback.
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
* For larger messages, only the header is downloaded and a placeholder is shown.
* These messages can be downloaded fully using dc_download_full_msg() later.
@@ -483,6 +508,16 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
+ * - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
+ * seconds. 2 days by default.
+ * This is not supposed to be changed by UIs and only used for testing.
+ * - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
+ * to 1 if it supports verified 1:1 chats.
+ * Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
+ * and when the key changes, an info message is posted into the chat.
+ * 0=Nothing else happens when the key changes.
+ * 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
+ * until `dc_accept_chat()` is called.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
@@ -807,7 +842,7 @@ void dc_maybe_network (dc_context_t* context);
* @param context The context as created by dc_context_new().
* @param addr The e-mail address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
- * @param public_data ASCII armored public key.
+ * @param public_data Ignored, actual public key is extracted from secret_data.
* @param secret_data ASCII armored secret key.
* @return 1 on success, 0 on failure.
*/
@@ -861,7 +896,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
* is added as needed.
* @param query_str An optional query for filtering the list. Only chats matching this query
- * are returned. Give NULL for no filtering.
+ * are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
+ * the chatlist is filtered such that only chats with unread messages show up.
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
* are returned. Give 0 for no filtering.
* @return A chatlist as an dc_chatlist_t object.
@@ -1101,7 +1137,7 @@ dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
*
* In JS land, that would be mapped to something as:
* ```
- * success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
+ * success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4');
* ```
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
* See dc_get_webxdc_status_updates() for the receiving counterpart.
@@ -1319,6 +1355,20 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
+/**
+ * Returns a list of similar chats.
+ *
+ * @warning This is an experimental API which may change or be removed in the future.
+ *
+ * @memberof dc_context_t
+ * @param context The context object as returned from dc_context_new().
+ * @param chat_id The ID of the chat for which to find similar chats.
+ * @return The list of similar chats.
+ * On errors, NULL is returned.
+ * Must be freed using dc_chatlist_unref() when no longer used.
+ */
+dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id);
+
/**
* Estimate the number of messages that will be deleted
@@ -1450,6 +1500,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
+ * @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The ID of the current message from which the next or previous message should be searched.
@@ -1468,24 +1519,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
-/**
- * Enable or disable protection against active attacks.
- * To enable protection, it is needed that all members are verified;
- * if this condition is met, end-to-end-encryption is always enabled
- * and only the verified keys are used.
- *
- * Sends out #DC_EVENT_CHAT_MODIFIED on changes
- * and #DC_EVENT_MSGS_CHANGED if a status message was sent.
- *
- * @memberof dc_context_t
- * @param context The context object as returned from dc_context_new().
- * @param chat_id The ID of the chat to change the protection for.
- * @param protect 1=protect chat, 0=unprotect chat
- * @return 1=success, 0=error, e.g. some members may be unverified
- */
-int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
-
-
/**
* Set chat visibility to pinned, archived or normal.
*
@@ -1679,24 +1712,12 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
- * however, recipients get the messages in normal one-to-one chats
- * and will not be aware of other members.
+ * however, recipients get the messages in a read-only chat
+ * and will see who the other members are.
*
- * Replies to broadcasts go only to the sender
- * and not to all broadcast recipients.
- * Moreover, replies will not appear in the broadcast list
- * but in the one-to-one chat with the person answering.
- *
- * The name and the image of the broadcast list is set automatically
- * and is visible to the sender only.
- * Not asking for these data allows more focused creation
- * and we bypass the question who will get which data.
- * Also, many users will have at most one broadcast list
- * so, a generic name and image is sufficient at the first place.
- *
- * Later on, however, the name can be changed using dc_set_chat_name().
- * The image cannot be changed to have a unique, recognizable icon in the chat lists.
- * All in all, this is also what other messengers are doing here.
+ * For historical reasons, this function does not take a name directly,
+ * instead you have to set the name using dc_set_chat_name()
+ * after creating the broadcast list.
*
* @memberof dc_context_t
* @param context The context object.
@@ -2239,8 +2260,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
* the backup is not encrypted.
* The backup contains all contacts, chats, images and other data and device independent settings.
* The backup does not contain device dependent settings as ringtones or LED notification settings.
- * The name of the backup is typically `delta-chat-.tar`, if more than one backup is create on a day,
- * the format is `delta-chat--.tar`
+ * The name of the backup is `delta-chat-backup---.tar`.
*
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
@@ -2253,6 +2273,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
*
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
+ * If `param1` is a filename, import the private key from the file and make it the default.
*
* While dc_imex() returns immediately, the started job may take a while,
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
@@ -2924,12 +2945,15 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
* @param dir The directory to create the context-databases in.
* If the directory does not exist,
* dc_accounts_new() will try to create it.
+ * @param writable Whether the returned account manager is writable, i.e. calling these functions on
+ * it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
+ * dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
* @return An account manager object.
* The object must be passed to the other account manager functions
* and must be freed using dc_accounts_unref() after usage.
* On errors, NULL is returned.
*/
-dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir);
+dc_accounts_t* dc_accounts_new (const char* dir, int writable);
/**
@@ -3710,7 +3734,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
* Check if a chat is protected.
* Protected chats contain only verified members and encryption is always enabled.
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
- * The status can be changed using dc_set_chat_protection().
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3719,6 +3742,26 @@ int dc_chat_can_send (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
+/**
+ * Checks if the chat was protected, and then an incoming message broke this protection.
+ *
+ * This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
+ * otherwise it will return false for all chats.
+ *
+ * 1:1 chats are automatically set as protected when a contact is verified.
+ * When a message comes in that is not encrypted / signed correctly,
+ * the chat is automatically set as unprotected again.
+ * dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
+ *
+ * The UI should let the user confirm that this is OK with a message like
+ * `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
+ * @memberof dc_chat_t
+ * @param chat The chat object.
+ * @return 1=chat protection broken, 0=otherwise.
+ */
+int dc_chat_is_protection_broken (const dc_chat_t* chat);
+
+
/**
* Check if locations are sent to the chat
* at the time the object was created using dc_get_chat().
@@ -3923,7 +3966,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
* Get the message time used for sorting.
* This function returns the timestamp that is used for sorting the message
* into lists as returned e.g. by dc_get_chat_msgs().
- * This may be the reveived time, the sending time or another time.
+ * This may be the received time, the sending time or another time.
*
* To get the receiving time, use dc_msg_get_received_timestamp().
* To get the sending time, use dc_msg_get_timestamp().
@@ -3976,16 +4019,17 @@ char* dc_msg_get_text (const dc_msg_t* msg);
*/
char* dc_msg_get_subject (const dc_msg_t* msg);
+
/**
- * Find out full path, file name and extension of the file associated with a
- * message.
+ * Find out full path of the file associated with a message.
*
* Typically files are associated with images, videos, audios, documents.
* Plain text messages do not have a file.
+ * File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename().
*
* @memberof dc_msg_t
* @param msg The message object.
- * @return The full path, the file name, and the extension of the file associated with the message.
+ * @return The full path (with file name and extension) of the file associated with the message.
* If there is no file associated with the message, an empty string is returned.
* NULL is never returned and the returned value must be released using dc_str_unref().
*/
@@ -3993,14 +4037,13 @@ char* dc_msg_get_file (const dc_msg_t* msg);
/**
- * Get a base file name without the path. The base file name includes the extension; the path
- * is not returned. To get the full path, use dc_msg_get_file().
+ * Get an original attachment filename, with extension but without the path. To get the full path,
+ * use dc_msg_get_file().
*
* @memberof dc_msg_t
* @param msg The message object.
- * @return The base file name plus the extension without part. If there is no file
- * associated with the message, an empty string is returned. The returned
- * value must be released using dc_str_unref().
+ * @return The attachment filename. If there is no file associated with the message, an empty string
+ * is returned. The returned value must be released using dc_str_unref().
*/
char* dc_msg_get_filename (const dc_msg_t* msg);
@@ -4313,7 +4356,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
* Check if the message is an informational message, created by the
* device or by another users. Such messages are not "typed" by the user but
* created due to other actions,
- * e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
+ * e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
* or dc_add_contact_to_chat().
*
* These messages are typically shown in the center of the chat view,
@@ -5007,7 +5050,12 @@ int dc_contact_is_verified (dc_contact_t* contact);
/**
* Return the address that verified a contact
*
- * The UI may use this in addition to a checkmark showing the verification status
+ * The UI may use this in addition to a checkmark showing the verification status.
+ * In case of verification chains,
+ * the last contact in the chain is shown.
+ * This is because of privacy reasons, but also as it would not help the user
+ * to see a unknown name here - where one can mostly always ask the shown name
+ * as it is directly known.
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5015,6 +5063,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
* A string containing the verifiers address. If it is the same address as the contact itself,
* we verified the contact ourself. If it is an empty string, we don't have verifier
* information or the contact is not verified.
+ * @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
*/
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
@@ -5027,7 +5076,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
* @memberof dc_contact_t
* @param contact The contact object.
* @return
- * The `ContactId` of the verifiers address. If it is the same address as the contact itself,
+ * The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
* we verified the contact ourself. If it is 0, we don't have verifier information or
* the contact is not verified.
*/
@@ -5127,6 +5176,72 @@ int dc_provider_get_status (const dc_provider_t* prov
void dc_provider_unref (dc_provider_t* provider);
+/**
+ * Return an HTTP(S) GET response.
+ * This function can be used to download remote content for HTML emails.
+ *
+ * @memberof dc_context_t
+ * @param context The context object to take proxy settings from.
+ * @param url HTTP or HTTPS URL.
+ * @return The response must be released using dc_http_response_unref() after usage.
+ * NULL is returned on errors.
+ */
+dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url);
+
+
+/**
+ * @class dc_http_response_t
+ *
+ * An object containing an HTTP(S) GET response.
+ * Created by dc_get_http_response().
+ */
+
+
+/**
+ * Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html".
+ *
+ * @memberof dc_http_response_t
+ * @param response HTTP response as returned by dc_get_http_response().
+ * @return The string which must be released using dc_str_unref() after usage. May be NULL.
+ */
+char* dc_http_response_get_mimetype (const dc_http_response_t* response);
+
+/**
+ * Returns HTTP response encoding, e.g. "utf-8".
+ *
+ * @memberof dc_http_response_t
+ * @param response HTTP response as returned by dc_get_http_response().
+ * @return The string which must be released using dc_str_unref() after usage. May be NULL.
+ */
+char* dc_http_response_get_encoding (const dc_http_response_t* response);
+
+/**
+ * Returns HTTP response contents.
+ *
+ * @memberof dc_http_response_t
+ * @param response HTTP response as returned by dc_get_http_response().
+ * @return The blob which must be released using dc_str_unref() after usage. NULL is never returned.
+ */
+uint8_t* dc_http_response_get_blob (const dc_http_response_t* response);
+
+/**
+ * Returns HTTP response content size.
+ *
+ * @memberof dc_http_response_t
+ * @param response HTTP response as returned by dc_get_http_response().
+ * @return The blob size.
+ */
+size_t dc_http_response_get_size (const dc_http_response_t* response);
+
+/**
+ * Free an HTTP response object.
+ *
+ * @memberof dc_http_response_t
+ * @param response HTTP response as returned by dc_get_http_response().
+ */
+void dc_http_response_unref (const dc_http_response_t* response);
+
+
/**
* @class dc_lot_t
*
@@ -5604,7 +5719,6 @@ void dc_reactions_unref (dc_reactions_t* reactions);
*/
-
/**
* @class dc_jsonrpc_instance_t
*
@@ -5653,6 +5767,17 @@ void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* req
*/
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
+/**
+ * Make a JSON-RPC call and return a response.
+ *
+ * @memberof dc_jsonrpc_instance_t
+ * @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
+ * @param input JSON-RPC request.
+ * @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
+ * If there is no response, NULL is returned.
+ */
+char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
+
/**
* @class dc_event_emitter_t
*
@@ -6000,6 +6125,15 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_MSG_READ 2015
+/**
+ * A single message is deleted.
+ *
+ * @param data1 (int) chat_id
+ * @param data2 (int) msg_id
+ */
+#define DC_EVENT_MSG_DELETED 2016
+
+
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
@@ -6178,6 +6312,7 @@ void dc_event_unref(dc_event_t* event);
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
+#define DC_KEY_GEN_RSA4096 3
/**
@@ -6661,15 +6796,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in error strings.
#define DC_STR_ERROR_NO_NETWORK 87
-/// "Chat protection enabled."
-///
-
-/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
-#define DC_STR_PROTECTION_ENABLED 88
-
-/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
-#define DC_STR_PROTECTION_DISABLED 89
-
/// "Reply"
///
/// Used in summaries.
@@ -7114,26 +7240,6 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
-/// "You enabled chat protection."
-///
-/// Used in status messages.
-#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
-
-/// "Chat protection enabled by %1$s."
-///
-/// `%1$s` will be replaced by name and address of the contact.
-///
-/// Used in status messages.
-#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
-
-/// "You disabled chat protection."
-#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
-
-/// "Chat protection disabled by %1$s."
-///
-/// `%1$s` will be replaced by name and address of the contact.
-#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
-
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
@@ -7144,6 +7250,16 @@ void dc_event_unref(dc_event_t* event);
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
+/// "Messages are guaranteed to be end-to-end encrypted from now on."
+///
+/// Used in info messages.
+#define DC_STR_CHAT_PROTECTION_ENABLED 170
+
+/// "%1$s sent a message from another device."
+///
+/// Used in info messages.
+#define DC_STR_CHAT_PROTECTION_DISABLED 171
+
/**
* @}
*/
diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs
index 6872c6d38..025c3f7ce 100644
--- a/deltachat-ffi/src/lib.rs
+++ b/deltachat-ffi/src/lib.rs
@@ -29,8 +29,9 @@ use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
-use deltachat::key::DcKey;
+use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
+use deltachat::net::read_url_blob;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
@@ -166,6 +167,24 @@ pub unsafe extern "C" fn dc_context_open(
.unwrap_or(0)
}
+#[no_mangle]
+pub unsafe extern "C" fn dc_context_change_passphrase(
+ context: *mut dc_context_t,
+ passphrase: *const libc::c_char,
+) -> libc::c_int {
+ if context.is_null() {
+ eprintln!("ignoring careless call to dc_context_change_passphrase()");
+ return 0;
+ }
+
+ let ctx = &*context;
+ let passphrase = to_string_lossy(passphrase);
+ block_on(ctx.change_passphrase(passphrase))
+ .context("dc_context_change_passphrase() failed")
+ .log_err(ctx)
+ .is_ok() as libc::c_int
+}
+
#[no_mangle]
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -526,6 +545,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
EventType::MsgRead { .. } => 2015,
+ EventType::MsgDeleted { .. } => 2016,
EventType::ChatModified(_) => 2020,
EventType::ChatEphemeralTimerModified { .. } => 2021,
EventType::ContactsChanged(_) => 2030,
@@ -573,6 +593,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::MsgDelivered { chat_id, .. }
| EventType::MsgFailed { chat_id, .. }
| EventType::MsgRead { chat_id, .. }
+ | EventType::MsgDeleted { chat_id, .. }
| EventType::ChatModified(chat_id)
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
@@ -630,7 +651,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
- | EventType::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
+ | EventType::MsgRead { msg_id, .. }
+ | EventType::MsgDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::SecurejoinInviterProgress { progress, .. }
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
@@ -673,6 +695,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::MsgDelivered { .. }
| EventType::MsgFailed { .. }
| EventType::MsgRead { .. }
+ | EventType::MsgDeleted { .. }
| EventType::ChatModified(_)
| EventType::ContactsChanged(_)
| EventType::LocationChanged(_)
@@ -782,7 +805,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
- public_data: *const libc::c_char,
+ _public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
@@ -790,21 +813,12 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
return 0;
}
let ctx = &*context;
- block_on(async move {
- let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
- let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
- let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
- let keypair = key::KeyPair {
- addr,
- public,
- secret,
- };
- key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
- Ok::<_, anyhow::Error>(1)
- })
- .context("Failed to save keypair")
- .log_err(ctx)
- .unwrap_or(0)
+ let addr = to_string_lossy(addr);
+ let secret_data = to_string_lossy(secret_data);
+ block_on(preconfigure_keypair(ctx, &addr, &secret_data))
+ .context("Failed to save keypair")
+ .log_err(ctx)
+ .is_ok() as libc::c_int
}
#[no_mangle]
@@ -1237,6 +1251,30 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
})
}
+#[no_mangle]
+pub unsafe extern "C" fn dc_get_similar_chatlist(
+ context: *mut dc_context_t,
+ chat_id: u32,
+) -> *mut dc_chatlist_t {
+ if context.is_null() {
+ eprintln!("ignoring careless call to dc_get_similar_chatlist()");
+ return ptr::null_mut();
+ }
+ let ctx = &*context;
+
+ let chat_id = ChatId::new(chat_id);
+ match block_on(chat_id.get_similar_chatlist(ctx))
+ .context("failed to get similar chatlist")
+ .log_err(ctx)
+ {
+ Ok(list) => {
+ let ffi_list = ChatlistWrapper { context, list };
+ Box::into_raw(Box::new(ffi_list))
+ }
+ Err(_) => ptr::null_mut(),
+ }
+}
+
#[no_mangle]
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
context: *mut dc_context_t,
@@ -1384,6 +1422,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
}
#[no_mangle]
+#[allow(deprecated)]
pub unsafe extern "C" fn dc_get_next_media(
context: *mut dc_context_t,
msg_id: u32,
@@ -1424,32 +1463,6 @@ pub unsafe extern "C" fn dc_get_next_media(
})
}
-#[no_mangle]
-pub unsafe extern "C" fn dc_set_chat_protection(
- context: *mut dc_context_t,
- chat_id: u32,
- protect: libc::c_int,
-) -> libc::c_int {
- if context.is_null() {
- eprintln!("ignoring careless call to dc_set_chat_protection()");
- return 0;
- }
- let ctx = &*context;
- let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
- s
- } else {
- warn!(ctx, "bad protect-value for dc_set_chat_protection()");
- return 0;
- };
-
- block_on(async move {
- match ChatId::new(chat_id).set_protection(ctx, protect).await {
- Ok(()) => 1,
- Err(_) => 0,
- }
- })
-}
-
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -1872,13 +1885,10 @@ pub unsafe extern "C" fn dc_get_msg_info(
return "".strdup();
}
let ctx = &*context;
-
- block_on(async move {
- message::get_msg_info(ctx, MsgId::new(msg_id))
- .await
- .unwrap_or_log_default(ctx, "failed to get msg id")
- .strdup()
- })
+ let msg_id = MsgId::new(msg_id);
+ block_on(msg_id.get_info(ctx))
+ .unwrap_or_log_default(ctx, "failed to get msg id")
+ .strdup()
}
#[no_mangle]
@@ -2523,7 +2533,12 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
- block_on(location::set(ctx, latitude, longitude, accuracy)) as _
+ block_on(async move {
+ location::set(ctx, latitude, longitude, accuracy)
+ .await
+ .log_err(ctx)
+ .unwrap_or_default()
+ }) as libc::c_int
}
#[no_mangle]
@@ -3082,6 +3097,16 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
ffi_chat.chat.is_protected() as libc::c_int
}
+#[no_mangle]
+pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
+ if chat.is_null() {
+ eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
+ return 0;
+ }
+ let ffi_chat = &*chat;
+ ffi_chat.chat.is_protection_broken() as libc::c_int
+}
+
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
@@ -3303,7 +3328,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
return "".strdup();
}
let ffi_msg = &*msg;
- ffi_msg.message.get_text().unwrap_or_default().strdup()
+ ffi_msg.message.get_text().strdup()
}
#[no_mangle]
@@ -3688,7 +3713,7 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::
return;
}
let ffi_msg = &mut *msg;
- ffi_msg.message.set_text(to_opt_string_lossy(text))
+ ffi_msg.message.set_text(to_string_lossy(text))
}
#[no_mangle]
@@ -4487,7 +4512,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
- match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
+ match block_on(provider::get_provider_info_by_addr(
+ ctx,
+ addr.as_str(),
+ true,
+ ))
+ .log_err(ctx)
+ .unwrap_or_default()
+ {
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4514,11 +4546,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
match socks5_enabled {
Ok(socks5_enabled) => {
- match block_on(provider::get_provider_info(
+ match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
socks5_enabled,
- )) {
+ ))
+ .log_err(ctx)
+ .unwrap_or_default()
+ {
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4572,6 +4607,96 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
// this may change once we start localizing string.
}
+// dc_http_response_t
+
+pub type dc_http_response_t = net::HttpResponse;
+
+#[no_mangle]
+pub unsafe extern "C" fn dc_get_http_response(
+ context: *const dc_context_t,
+ url: *const libc::c_char,
+) -> *mut dc_http_response_t {
+ if context.is_null() || url.is_null() {
+ eprintln!("ignoring careless call to dc_get_http_response()");
+ return ptr::null_mut();
+ }
+
+ let context = &*context;
+ let url = to_string_lossy(url);
+ if let Ok(response) = block_on(read_url_blob(context, &url))
+ .context("read_url_blob")
+ .log_err(context)
+ {
+ Box::into_raw(Box::new(response))
+ } else {
+ ptr::null_mut()
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn dc_http_response_get_mimetype(
+ response: *const dc_http_response_t,
+) -> *mut libc::c_char {
+ if response.is_null() {
+ eprintln!("ignoring careless call to dc_http_response_get_mimetype()");
+ return ptr::null_mut();
+ }
+
+ let response = &*response;
+ response.mimetype.strdup()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn dc_http_response_get_encoding(
+ response: *const dc_http_response_t,
+) -> *mut libc::c_char {
+ if response.is_null() {
+ eprintln!("ignoring careless call to dc_http_response_get_encoding()");
+ return ptr::null_mut();
+ }
+
+ let response = &*response;
+ response.encoding.strdup()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn dc_http_response_get_blob(
+ response: *const dc_http_response_t,
+) -> *mut libc::c_char {
+ if response.is_null() {
+ eprintln!("ignoring careless call to dc_http_response_get_blob()");
+ return ptr::null_mut();
+ }
+
+ let response = &*response;
+ let blob_len = response.blob.len();
+ let ptr = libc::malloc(blob_len);
+ libc::memcpy(ptr, response.blob.as_ptr() as *mut libc::c_void, blob_len);
+ ptr as *mut libc::c_char
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn dc_http_response_get_size(
+ response: *const dc_http_response_t,
+) -> libc::size_t {
+ if response.is_null() {
+ eprintln!("ignoring careless call to dc_http_response_get_size()");
+ return 0;
+ }
+
+ let response = &*response;
+ response.blob.len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn dc_http_response_unref(response: *mut dc_http_response_t) {
+ if response.is_null() {
+ eprintln!("ignoring careless call to dc_http_response_unref()");
+ return;
+ }
+ drop(Box::from_raw(response));
+}
+
// -- Accounts
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
@@ -4600,17 +4725,17 @@ pub type dc_accounts_t = AccountsWrapper;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new(
- _os_name: *const libc::c_char,
- dbfile: *const libc::c_char,
+ dir: *const libc::c_char,
+ writable: libc::c_int,
) -> *mut dc_accounts_t {
setup_panic!();
- if dbfile.is_null() {
+ if dir.is_null() {
eprintln!("ignoring careless call to dc_accounts_new()");
return ptr::null_mut();
}
- let accs = block_on(Accounts::new(as_path(dbfile).into()));
+ let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
match accs {
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
@@ -4876,7 +5001,6 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use deltachat_jsonrpc::api::CommandApi;
- use deltachat_jsonrpc::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use super::*;
@@ -4884,7 +5008,6 @@ mod jsonrpc {
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession,
- event_thread: JoinHandle>,
}
#[no_mangle]
@@ -4897,28 +5020,12 @@ mod jsonrpc {
}
let account_manager = &*account_manager;
- let events = block_on(account_manager.read()).get_event_emitter();
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
let (request_handle, receiver) = RpcClient::new();
- let handle = RpcSession::new(request_handle.clone(), cmd_api);
+ let handle = RpcSession::new(request_handle, cmd_api);
- let event_thread = spawn(async move {
- while let Some(event) = events.recv().await {
- let event = event_to_json_rpc_notification(event);
- request_handle
- .send_notification("event", Some(event))
- .await?;
- }
- let res: Result<(), anyhow::Error> = Ok(());
- res
- });
-
- let instance = dc_jsonrpc_instance_t {
- receiver,
- handle,
- event_thread,
- };
+ let instance = dc_jsonrpc_instance_t { receiver, handle };
Box::into_raw(Box::new(instance))
}
@@ -4929,7 +5036,6 @@ mod jsonrpc {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
- (*jsonrpc_instance).event_thread.abort();
drop(Box::from_raw(jsonrpc_instance));
}
@@ -4967,4 +5073,28 @@ mod jsonrpc {
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
+
+ #[no_mangle]
+ pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
+ jsonrpc_instance: *mut dc_jsonrpc_instance_t,
+ input: *const libc::c_char,
+ ) -> *mut libc::c_char {
+ if jsonrpc_instance.is_null() {
+ eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
+ return ptr::null_mut();
+ }
+ let api = &*jsonrpc_instance;
+ let input = to_string_lossy(input);
+ let res = block_on(api.handle.process_incoming(&input));
+ match res {
+ Some(message) => {
+ if let Ok(message) = serde_json::to_string(&message) {
+ message.strdup()
+ } else {
+ ptr::null_mut()
+ }
+ }
+ None => ptr::null_mut(),
+ }
+ }
}
diff --git a/deltachat-jsonrpc/.gitignore b/deltachat-jsonrpc/.gitignore
index c12c4a8ba..33653476a 100644
--- a/deltachat-jsonrpc/.gitignore
+++ b/deltachat-jsonrpc/.gitignore
@@ -1,3 +1,4 @@
+openrpc/openrpc.json
accounts/
.cargo
\ No newline at end of file
diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml
index 77787abd3..ec2900f09 100644
--- a/deltachat-jsonrpc/Cargo.toml
+++ b/deltachat-jsonrpc/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
-version = "1.112.6"
+version = "1.126.1"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -15,25 +15,26 @@ required-features = ["webserver"]
anyhow = "1"
deltachat = { path = ".." }
num-traits = "0.2"
+schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
-tempfile = "3.3.0"
+tempfile = "3.8.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
-serde_json = "1.0.95"
-yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
-typescript-type-def = { version = "0.5.5", features = ["json_value"] }
-tokio = { version = "1.27.0" }
-sanitize-filename = "0.4"
+serde_json = "1.0.105"
+yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
+typescript-type-def = { version = "0.5.8", features = ["json_value"] }
+tokio = { version = "1.32.0" }
+sanitize-filename = "0.5"
walkdir = "2.3.3"
base64 = "0.21"
# optional dependencies
-axum = { version = "0.6.12", optional = true, features = ["ws"] }
+axum = { version = "0.6.20", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
-tokio = { version = "1.27.0", features = ["full", "rt-multi-thread"] }
+tokio = { version = "1.32.0", features = ["full", "rt-multi-thread"] }
[features]
diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs
index cc994ed63..b67c8e595 100644
--- a/deltachat-jsonrpc/src/api/mod.rs
+++ b/deltachat-jsonrpc/src/api/mod.rs
@@ -4,48 +4,47 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
-use deltachat::qr::Qr;
-use deltachat::{
- chat::{
- self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
- marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
- ProtectionStatus,
- },
- chatlist::Chatlist,
- config::Config,
- constants::DC_MSG_ID_DAYMARKER,
- contact::{may_be_valid_addr, Contact, ContactId, Origin},
- context::get_info,
- ephemeral::Timer,
- imex, location,
- message::{
- self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype,
- },
- provider::get_provider_info,
- qr,
- qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
- reaction::send_reaction,
- securejoin,
- stock_str::StockMessage,
- webxdc::StatusUpdateSerial,
+use deltachat::chat::{
+ self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
+ marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
+ ProtectionStatus,
};
+use deltachat::chatlist::Chatlist;
+use deltachat::config::Config;
+use deltachat::constants::DC_MSG_ID_DAYMARKER;
+use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
+use deltachat::context::get_info;
+use deltachat::ephemeral::Timer;
+use deltachat::imex;
+use deltachat::location;
+use deltachat::message::get_msg_read_receipts;
+use deltachat::message::{
+ self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
+};
+use deltachat::provider::get_provider_info;
+use deltachat::qr::{self, Qr};
+use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
+use deltachat::reaction::{get_msg_reactions, send_reaction};
+use deltachat::securejoin;
+use deltachat::stock_str::StockMessage;
+use deltachat::webxdc::StatusUpdateSerial;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
-pub mod events;
pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
-use types::chat_list::ChatListEntry;
use types::contact::ContactObject;
-use types::message::MessageData;
-use types::message::MessageObject;
+use types::events::Event;
+use types::http::HttpResponse;
+use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::provider_info::ProviderInfo;
+use types::reactions::JSONRPCReactions;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageLoadResult;
@@ -154,16 +153,26 @@ impl CommandApi {
// Misc top level functions
// ---------------------------------------------
- /// Check if an email address is valid.
+ /// Checks if an email address is valid.
async fn check_email_validity(&self, email: String) -> bool {
may_be_valid_addr(&email)
}
- /// Get general system info.
+ /// Returns general system info.
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
get_info()
}
+ /// Get the next event.
+ async fn get_next_event(&self) -> Result {
+ let event_emitter = self.accounts.read().await.get_event_emitter();
+ event_emitter
+ .recv()
+ .await
+ .map(|event| event.into())
+ .context("event channel is closed")
+ }
+
// ---------------------------------------------
// Account Management
// ---------------------------------------------
@@ -205,18 +214,18 @@ impl CommandApi {
let context_option = self.accounts.read().await.get_account(id);
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
- } else {
- println!("account with id {id} doesn't exist anymore");
}
}
Ok(accounts)
}
+ /// Starts background tasks for all accounts.
async fn start_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.start_io().await;
Ok(())
}
+ /// Stops background tasks for all accounts.
async fn stop_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.stop_io().await;
Ok(())
@@ -226,14 +235,16 @@ impl CommandApi {
// Methods that work on individual accounts
// ---------------------------------------------
- async fn start_io(&self, id: u32) -> Result<()> {
- let ctx = self.get_context(id).await?;
+ /// Starts background tasks for a single account.
+ async fn start_io(&self, account_id: u32) -> Result<()> {
+ let ctx = self.get_context(account_id).await?;
ctx.start_io().await;
Ok(())
}
- async fn stop_io(&self, id: u32) -> Result<()> {
- let ctx = self.get_context(id).await?;
+ /// Stops background tasks for a single account.
+ async fn stop_io(&self, account_id: u32) -> Result<()> {
+ let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
Ok(())
}
@@ -300,11 +311,13 @@ impl CommandApi {
ctx.get_info().await
}
+ /// Sets the given configuration key.
async fn set_config(&self, account_id: u32, key: String, value: Option) -> Result<()> {
let ctx = self.get_context(account_id).await?;
set_config(&ctx, &key, value.as_deref()).await
}
+ /// Updates a batch of configuration values.
async fn batch_set_config(
&self,
account_id: u32,
@@ -336,6 +349,7 @@ impl CommandApi {
Ok(qr_object)
}
+ /// Returns configuration value for the given key.
async fn get_config(&self, account_id: u32, key: String) -> Result> {
let ctx = self.get_context(account_id).await?;
get_config(&ctx, &key).await
@@ -539,7 +553,7 @@ impl CommandApi {
list_flags: Option,
query_string: Option,
query_contact_id: Option,
- ) -> Result> {
+ ) -> Result> {
let ctx = self.get_context(account_id).await?;
let list = Chatlist::try_load(
&ctx,
@@ -548,32 +562,43 @@ impl CommandApi {
query_contact_id.map(ContactId::new),
)
.await?;
- let mut l: Vec = Vec::with_capacity(list.len());
+ let mut l: Vec = Vec::with_capacity(list.len());
for i in 0..list.len() {
- l.push(ChatListEntry(
- list.get_chat_id(i)?.to_u32(),
- list.get_msg_id(i)?.unwrap_or_default().to_u32(),
- ));
+ l.push(list.get_chat_id(i)?.to_u32());
}
Ok(l)
}
+ /// Returns chats similar to the given one.
+ ///
+ /// Experimental API, subject to change without notice.
+ async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result> {
+ let ctx = self.get_context(account_id).await?;
+ let chat_id = ChatId::new(chat_id);
+ let list = chat_id
+ .get_similar_chat_ids(&ctx)
+ .await?
+ .into_iter()
+ .map(|(chat_id, _metric)| chat_id.to_u32())
+ .collect();
+ Ok(list)
+ }
+
async fn get_chatlist_items_by_entries(
&self,
account_id: u32,
- entries: Vec,
+ entries: Vec,
) -> Result> {
- // todo custom json deserializer for ChatListEntry?
let ctx = self.get_context(account_id).await?;
let mut result: HashMap =
HashMap::with_capacity(entries.len());
- for entry in entries.iter() {
+ for &entry in entries.iter() {
result.insert(
- entry.0,
+ entry,
match get_chat_list_item_by_id(&ctx, entry).await {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
- id: entry.0,
+ id: entry,
error: format!("{err:#}"),
},
},
@@ -790,24 +815,12 @@ impl CommandApi {
/// Create a new broadcast list.
///
/// Broadcast lists are similar to groups on the sending device,
- /// however, recipients get the messages in normal one-to-one chats
- /// and will not be aware of other members.
+ /// however, recipients get the messages in a read-only chat
+ /// and will see who the other members are.
///
- /// Replies to broadcasts go only to the sender
- /// and not to all broadcast recipients.
- /// Moreover, replies will not appear in the broadcast list
- /// but in the one-to-one chat with the person answering.
- ///
- /// The name and the image of the broadcast list is set automatically
- /// and is visible to the sender only.
- /// Not asking for these data allows more focused creation
- /// and we bypass the question who will get which data.
- /// Also, many users will have at most one broadcast list
- /// so, a generic name and image is sufficient at the first place.
- ///
- /// Later on, however, the name can be changed using dc_set_chat_name().
- /// The image cannot be changed to have a unique, recognizable icon in the chat lists.
- /// All in all, this is also what other messengers are doing here.
+ /// For historical reasons, this function does not take a name directly,
+ /// instead you have to set the name using dc_set_chat_name()
+ /// after creating the broadcast list.
async fn create_broadcast_list(&self, account_id: u32) -> Result {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast_list(&ctx)
@@ -892,7 +905,7 @@ impl CommandApi {
) -> Result {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some(text));
+ msg.set_text(text);
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
Ok(message_id.to_u32())
@@ -1110,7 +1123,25 @@ impl CommandApi {
/// max. text returned by dc_msg_get_text() (about 30000 characters).
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result {
let ctx = self.get_context(account_id).await?;
- get_msg_info(&ctx, MsgId::new(message_id)).await
+ MsgId::new(message_id).get_info(&ctx).await
+ }
+
+ /// Returns contacts that sent read receipts and the time of reading.
+ async fn get_message_read_receipts(
+ &self,
+ account_id: u32,
+ message_id: u32,
+ ) -> Result> {
+ let ctx = self.get_context(account_id).await?;
+ let receipts = get_msg_read_receipts(&ctx, MsgId::new(message_id))
+ .await?
+ .iter()
+ .map(|(contact_id, ts)| MessageReadReceipt {
+ contact_id: contact_id.to_u32(),
+ timestamp: *ts,
+ })
+ .collect();
+ Ok(receipts)
}
/// Asks the core to start downloading a message fully.
@@ -1313,7 +1344,7 @@ impl CommandApi {
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
- let contact = Contact::load_from_db(&ctx, contact_id).await?;
+ let contact = Contact::get_by_id(&ctx, contact_id).await?;
let addr = contact.get_addr();
Contact::create(&ctx, &name, addr).await?;
Ok(())
@@ -1387,6 +1418,10 @@ impl CommandApi {
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
+ ///
+ /// Deprecated 2023-10-03, use `get_chat_media` method
+ /// and navigate the returned array instead.
+ #[allow(deprecated)]
async fn get_neighboring_chat_media(
&self,
account_id: u32,
@@ -1658,6 +1693,15 @@ impl CommandApi {
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
+ /// Makes an HTTP GET request and returns a response.
+ ///
+ /// `url` is the HTTP or HTTPS URL.
+ async fn get_http_response(&self, account_id: u32, url: String) -> Result {
+ let ctx = self.get_context(account_id).await?;
+ let response = deltachat::net::read_url_blob(&ctx, &url).await?.into();
+ Ok(response)
+ }
+
/// Forward messages to another chat.
///
/// All types of messages can be forwarded,
@@ -1675,6 +1719,20 @@ impl CommandApi {
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
+ /// Resend messages and make information available for newly added chat members.
+ /// Resending sends out the original message, however, recipients and webxdc-status may differ.
+ /// Clients that already have the original message can still ignore the resent message as
+ /// they have tracked the state by dedicated updates.
+ ///
+ /// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
+ ///
+ /// message_ids all message IDs that should be resend. All messages must belong to the same chat.
+ async fn resend_messages(&self, account_id: u32, message_ids: Vec) -> Result<()> {
+ let ctx = self.get_context(account_id).await?;
+ let message_ids: Vec = message_ids.into_iter().map(MsgId::new).collect();
+ chat::resend_msgs(&ctx, &message_ids).await
+ }
+
async fn send_sticker(
&self,
account_id: u32,
@@ -1686,6 +1744,9 @@ impl CommandApi {
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(&sticker_path, None);
+ // JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
+ msg.force_sticker();
+
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
@@ -1707,6 +1768,21 @@ impl CommandApi {
Ok(message_id.to_u32())
}
+ /// Returns reactions to the message.
+ async fn get_message_reactions(
+ &self,
+ account_id: u32,
+ message_id: u32,
+ ) -> Result> {
+ let ctx = self.get_context(account_id).await?;
+ let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
+ if reactions.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(reactions.into()))
+ }
+ }
+
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result {
let ctx = self.get_context(account_id).await?;
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
@@ -1716,9 +1792,7 @@ impl CommandApi {
} else {
Viewtype::Text
});
- if data.text.is_some() {
- message.set_text(data.text);
- }
+ message.set_text(data.text.unwrap_or_default());
if data.html.is_some() {
message.set_html(data.html);
}
@@ -1806,7 +1880,7 @@ impl CommandApi {
.context("path conversion to string failed")
}
- /// save a sticker to a collection/folder in the account's sticker folder
+ /// Saves a sticker to a collection/folder in the account's sticker folder.
async fn misc_save_sticker(
&self,
account_id: u32,
@@ -1899,7 +1973,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some(text));
+ msg.set_text(text);
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
@@ -1922,9 +1996,7 @@ impl CommandApi {
} else {
Viewtype::Text
});
- if text.is_some() {
- message.set_text(text);
- }
+ message.set_text(text.unwrap_or_default());
if let Some(file) = file {
message.set_file(file, None);
}
@@ -1968,9 +2040,7 @@ impl CommandApi {
} else {
Viewtype::Text
});
- if text.is_some() {
- draft.set_text(text);
- }
+ draft.set_text(text.unwrap_or_default());
if let Some(file) = file {
draft.set_file(file, None);
}
@@ -1989,6 +2059,23 @@ impl CommandApi {
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
}
+
+ // send the chat's current set draft
+ async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result {
+ let ctx = self.get_context(account_id).await?;
+ if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
+ let mut draft = draft;
+ let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
+ .await?
+ .to_u32();
+ Ok(msg_id)
+ } else {
+ Err(anyhow!(
+ "chat with id {} doesn't have draft message",
+ chat_id
+ ))
+ }
+ }
}
// Helper functions (to prevent code duplication)
diff --git a/deltachat-jsonrpc/src/api/types/account.rs b/deltachat-jsonrpc/src/api/types/account.rs
index 8e0f80aef..b2909109d 100644
--- a/deltachat-jsonrpc/src/api/types/account.rs
+++ b/deltachat-jsonrpc/src/api/types/account.rs
@@ -6,8 +6,8 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
-#[derive(Serialize, TypeDef)]
-#[serde(tag = "type")]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
+#[serde(tag = "kind")]
pub enum Account {
#[serde(rename_all = "camelCase")]
Configured {
diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs
index f5a5c3bcd..e2146c8c1 100644
--- a/deltachat-jsonrpc/src/api/types/chat.rs
+++ b/deltachat-jsonrpc/src/api/types/chat.rs
@@ -1,6 +1,6 @@
use std::time::{Duration, SystemTime};
-use anyhow::{anyhow, bail, Result};
+use anyhow::{bail, Context as _, Result};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
@@ -13,7 +13,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct FullChat {
id: u32,
@@ -53,7 +53,9 @@ impl FullChat {
contacts.push(
ContactObject::try_from_dc_contact(
context,
- Contact::load_from_db(context, *contact_id).await?,
+ Contact::get_by_id(context, *contact_id)
+ .await
+ .context("failed to load contact")?,
)
.await?,
)
@@ -72,8 +74,9 @@ impl FullChat {
let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.get(0) {
- Some(contact) => Contact::load_from_db(context, *contact)
- .await?
+ Some(contact) => Contact::get_by_id(context, *contact)
+ .await
+ .context("failed to load contact for was_seen_recently")?
.was_seen_recently(),
None => false,
}
@@ -89,10 +92,7 @@ impl FullChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
- chat_type: chat
- .get_type()
- .to_u32()
- .ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
+ chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
@@ -121,7 +121,7 @@ impl FullChat {
/// - can_send
///
/// used when you only need the basic metadata of a chat like type, name, profile picture
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BasicChat {
id: u32,
@@ -155,10 +155,7 @@ impl BasicChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
- chat_type: chat
- .get_type()
- .to_u32()
- .ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
+ chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
color,
@@ -169,11 +166,12 @@ impl BasicChat {
}
}
-#[derive(Clone, Serialize, Deserialize, TypeDef)]
+#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
+#[serde(tag = "kind")]
pub enum MuteDuration {
NotMuted,
Forever,
- Until(i64),
+ Until { duration: i64 },
}
impl MuteDuration {
@@ -181,20 +179,20 @@ impl MuteDuration {
match self {
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
- MuteDuration::Until(n) => {
- if n <= 0 {
+ MuteDuration::Until { duration } => {
+ if duration <= 0 {
bail!("failed to read mute duration")
}
Ok(SystemTime::now()
- .checked_add(Duration::from_secs(n as u64))
+ .checked_add(Duration::from_secs(duration as u64))
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
}
}
}
}
-#[derive(Clone, Serialize, Deserialize, TypeDef)]
+#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "ChatVisibility")]
pub enum JSONRPCChatVisibility {
Normal,
diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs
index 83b84dd3d..b47f7a728 100644
--- a/deltachat-jsonrpc/src/api/types/chat_list.rs
+++ b/deltachat-jsonrpc/src/api/types/chat_list.rs
@@ -1,25 +1,21 @@
-use anyhow::Result;
+use anyhow::{Context, Result};
+use deltachat::chat::{Chat, ChatId};
+use deltachat::chatlist::get_last_message_for_chat;
use deltachat::constants::*;
use deltachat::contact::{Contact, ContactId};
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
};
-use deltachat::{
- chat::{Chat, ChatId},
- message::MsgId,
-};
use num_traits::cast::ToPrimitive;
-use serde::{Deserialize, Serialize};
+use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
+use super::message::MessageViewtype;
-#[derive(Deserialize, Serialize, TypeDef)]
-pub struct ChatListEntry(pub u32, pub u32);
-
-#[derive(Serialize, TypeDef)]
-#[serde(tag = "type")]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
+#[serde(tag = "kind")]
pub enum ChatListItemFetchResult {
#[serde(rename_all = "camelCase")]
ChatListItem {
@@ -31,6 +27,8 @@ pub enum ChatListItemFetchResult {
summary_text1: String,
summary_text2: String,
summary_status: u32,
+ /// showing preview if last chat message is image
+ summary_preview_image: Option,
is_protected: bool,
is_group: bool,
fresh_message_counter: usize,
@@ -47,6 +45,8 @@ pub enum ChatListItemFetchResult {
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option,
was_seen_recently: bool,
+ last_message_type: Option,
+ last_message_id: Option,
},
#[serde(rename_all = "camelCase")]
ArchiveLink { fresh_message_counter: usize },
@@ -56,14 +56,9 @@ pub enum ChatListItemFetchResult {
pub(crate) async fn get_chat_list_item_by_id(
ctx: &deltachat::context::Context,
- entry: &ChatListEntry,
+ entry: u32,
) -> Result {
- let chat_id = ChatId::new(entry.0);
- let last_msgid = match entry.1 {
- 0 => None,
- _ => Some(MsgId::new(entry.1)),
- };
-
+ let chat_id = ChatId::new(entry);
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
if chat_id.is_archived_link() {
@@ -72,12 +67,18 @@ pub(crate) async fn get_chat_list_item_by_id(
});
}
- let chat = Chat::load_from_db(ctx, chat_id).await?;
- let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
+ let last_msgid = get_last_message_for_chat(ctx, chat_id).await?;
+
+ let chat = Chat::load_from_db(ctx, chat_id).await.context("chat")?;
+ let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat))
+ .await
+ .context("summary")?;
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
let summary_text2 = summary.text.to_owned();
+ let summary_preview_image = summary.thumbnail_path;
+
let visibility = chat.get_visibility();
let avatar_path = chat
@@ -85,12 +86,15 @@ pub(crate) async fn get_chat_list_item_by_id(
.await?
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
- let last_updated = match last_msgid {
+ let (last_updated, message_type) = match last_msgid {
Some(id) => {
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
- Some(last_message.get_timestamp() * 1000)
+ (
+ Some(last_message.get_timestamp() * 1000),
+ Some(last_message.get_viewtype().into()),
+ )
}
- None => None,
+ None => (None, None),
};
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
@@ -100,8 +104,9 @@ pub(crate) async fn get_chat_list_item_by_id(
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
let contact = chat_contacts.get(0);
let was_seen_recently = match contact {
- Some(contact) => Contact::load_from_db(ctx, *contact)
- .await?
+ Some(contact) => Contact::get_by_id(ctx, *contact)
+ .await
+ .context("contact")?
.was_seen_recently(),
None => false,
};
@@ -124,6 +129,7 @@ pub(crate) async fn get_chat_list_item_by_id(
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
+ summary_preview_image,
is_protected: chat.is_protected(),
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
@@ -138,5 +144,7 @@ pub(crate) async fn get_chat_list_item_by_id(
is_broadcast: chat.get_type() == Chattype::Broadcast,
dm_chat_contact,
was_seen_recently,
+ last_message_type: message_type,
+ last_message_id: last_msgid.map(|id| id.to_u32()),
})
}
diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs
index d67fc7fb1..53b0ef67c 100644
--- a/deltachat-jsonrpc/src/api/types/contact.rs
+++ b/deltachat-jsonrpc/src/api/types/contact.rs
@@ -6,7 +6,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Contact", rename_all = "camelCase")]
pub struct ContactObject {
address: String,
diff --git a/deltachat-jsonrpc/src/api/events.rs b/deltachat-jsonrpc/src/api/types/events.rs
similarity index 77%
rename from deltachat-jsonrpc/src/api/events.rs
rename to deltachat-jsonrpc/src/api/types/events.rs
index ddbbcebca..dd03358bc 100644
--- a/deltachat-jsonrpc/src/api/events.rs
+++ b/deltachat-jsonrpc/src/api/types/events.rs
@@ -1,19 +1,29 @@
-use deltachat::{Event, EventType};
+use deltachat::{Event as CoreEvent, EventType as CoreEventType};
use serde::Serialize;
-use serde_json::{json, Value};
use typescript_type_def::TypeDef;
-pub fn event_to_json_rpc_notification(event: Event) -> Value {
- let id: JSONRPCEventType = event.typ.into();
- json!({
- "event": id,
- "contextId": event.id,
- })
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct Event {
+ /// Event payload.
+ event: EventType,
+
+ /// Account ID.
+ context_id: u32,
}
-#[derive(Serialize, TypeDef)]
-#[serde(tag = "type", rename = "Event")]
-pub enum JSONRPCEventType {
+impl From for Event {
+ fn from(event: CoreEvent) -> Self {
+ Event {
+ event: event.typ.into(),
+ context_id: event.id,
+ }
+ }
+}
+
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
+#[serde(tag = "kind")]
+pub enum EventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
@@ -164,6 +174,13 @@ pub enum JSONRPCEventType {
msg_id: u32,
},
+ /// A single message is deleted.
+ #[serde(rename_all = "camelCase")]
+ MsgDeleted {
+ chat_id: u32,
+ msg_id: u32,
+ },
+
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See setChatName(), setChatProfileImage(), addContactToChat()
@@ -286,27 +303,27 @@ pub enum JSONRPCEventType {
},
}
-impl From for JSONRPCEventType {
- fn from(event: EventType) -> Self {
- use JSONRPCEventType::*;
+impl From for EventType {
+ fn from(event: CoreEventType) -> Self {
+ use EventType::*;
match event {
- EventType::Info(msg) => Info { msg },
- EventType::SmtpConnected(msg) => SmtpConnected { msg },
- EventType::ImapConnected(msg) => ImapConnected { msg },
- EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
- EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
- EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
- EventType::ImapInboxIdle => ImapInboxIdle,
- EventType::NewBlobFile(file) => NewBlobFile { file },
- EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
- EventType::Warning(msg) => Warning { msg },
- EventType::Error(msg) => Error { msg },
- EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
- EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
+ CoreEventType::Info(msg) => Info { msg },
+ CoreEventType::SmtpConnected(msg) => SmtpConnected { msg },
+ CoreEventType::ImapConnected(msg) => ImapConnected { msg },
+ CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
+ CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
+ CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
+ CoreEventType::ImapInboxIdle => ImapInboxIdle,
+ CoreEventType::NewBlobFile(file) => NewBlobFile { file },
+ CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
+ CoreEventType::Warning(msg) => Warning { msg },
+ CoreEventType::Error(msg) => Error { msg },
+ CoreEventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
+ CoreEventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
- EventType::ReactionsChanged {
+ CoreEventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
@@ -315,92 +332,80 @@ impl From for JSONRPCEventType {
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
- EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
+ CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
- EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
+ CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
- EventType::MsgsNoticed(chat_id) => MsgsNoticed {
+ CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},
- EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
+ CoreEventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
- EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
+ CoreEventType::MsgFailed { chat_id, msg_id } => MsgFailed {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
- EventType::MsgRead { chat_id, msg_id } => MsgRead {
+ CoreEventType::MsgRead { chat_id, msg_id } => MsgRead {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
- EventType::ChatModified(chat_id) => ChatModified {
+ CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
+ chat_id: chat_id.to_u32(),
+ msg_id: msg_id.to_u32(),
+ },
+ CoreEventType::ChatModified(chat_id) => ChatModified {
chat_id: chat_id.to_u32(),
},
- EventType::ChatEphemeralTimerModified { chat_id, timer } => {
+ CoreEventType::ChatEphemeralTimerModified { chat_id, timer } => {
ChatEphemeralTimerModified {
chat_id: chat_id.to_u32(),
timer: timer.to_u32(),
}
}
- EventType::ContactsChanged(contact) => ContactsChanged {
+ CoreEventType::ContactsChanged(contact) => ContactsChanged {
contact_id: contact.map(|c| c.to_u32()),
},
- EventType::LocationChanged(contact) => LocationChanged {
+ CoreEventType::LocationChanged(contact) => LocationChanged {
contact_id: contact.map(|c| c.to_u32()),
},
- EventType::ConfigureProgress { progress, comment } => {
+ CoreEventType::ConfigureProgress { progress, comment } => {
ConfigureProgress { progress, comment }
}
- EventType::ImexProgress(progress) => ImexProgress { progress },
- EventType::ImexFileWritten(path) => ImexFileWritten {
+ CoreEventType::ImexProgress(progress) => ImexProgress { progress },
+ CoreEventType::ImexFileWritten(path) => ImexFileWritten {
path: path.to_str().unwrap_or_default().to_owned(),
},
- EventType::SecurejoinInviterProgress {
+ CoreEventType::SecurejoinInviterProgress {
contact_id,
progress,
} => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(),
progress,
},
- EventType::SecurejoinJoinerProgress {
+ CoreEventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => SecurejoinJoinerProgress {
contact_id: contact_id.to_u32(),
progress,
},
- EventType::ConnectivityChanged => ConnectivityChanged,
- EventType::SelfavatarChanged => SelfavatarChanged,
- EventType::WebxdcStatusUpdate {
+ CoreEventType::ConnectivityChanged => ConnectivityChanged,
+ CoreEventType::SelfavatarChanged => SelfavatarChanged,
+ CoreEventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
} => WebxdcStatusUpdate {
msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(),
},
- EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
+ CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
}
}
}
-
-#[cfg(test)]
-#[test]
-fn generate_events_ts_types_definition() {
- let events = {
- let mut buf = Vec::new();
- let options = typescript_type_def::DefinitionFileOptions {
- root_namespace: None,
- ..typescript_type_def::DefinitionFileOptions::default()
- };
- typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options)
- .unwrap();
- String::from_utf8(buf).unwrap()
- };
- std::fs::write("typescript/generated/events.ts", events).unwrap();
-}
diff --git a/deltachat-jsonrpc/src/api/types/http.rs b/deltachat-jsonrpc/src/api/types/http.rs
new file mode 100644
index 000000000..9121a677e
--- /dev/null
+++ b/deltachat-jsonrpc/src/api/types/http.rs
@@ -0,0 +1,29 @@
+use deltachat::net::HttpResponse as CoreHttpResponse;
+use serde::Serialize;
+use typescript_type_def::TypeDef;
+
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
+pub struct HttpResponse {
+ /// base64-encoded response body.
+ blob: String,
+
+ /// MIME type, e.g. "text/plain" or "text/html".
+ mimetype: Option,
+
+ /// Encoding, e.g. "utf-8".
+ encoding: Option,
+}
+
+impl From for HttpResponse {
+ fn from(response: CoreHttpResponse) -> Self {
+ use base64::{engine::general_purpose, Engine as _};
+ let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
+ let mimetype = response.mimetype;
+ let encoding = response.encoding;
+ HttpResponse {
+ blob,
+ mimetype,
+ encoding,
+ }
+ }
+}
diff --git a/deltachat-jsonrpc/src/api/types/location.rs b/deltachat-jsonrpc/src/api/types/location.rs
index 374e408d8..f610b42d3 100644
--- a/deltachat-jsonrpc/src/api/types/location.rs
+++ b/deltachat-jsonrpc/src/api/types/location.rs
@@ -2,7 +2,7 @@ use deltachat::location::Location;
use serde::Serialize;
use typescript_type_def::TypeDef;
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Location", rename_all = "camelCase")]
pub struct JsonrpcLocation {
pub location_id: u32,
diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs
index 44d2a1f05..4bd691e3a 100644
--- a/deltachat-jsonrpc/src/api/types/message.rs
+++ b/deltachat-jsonrpc/src/api/types/message.rs
@@ -1,7 +1,7 @@
-use anyhow::{anyhow, Result};
+use anyhow::{Context as _, Result};
use deltachat::chat::Chat;
use deltachat::chat::ChatItem;
-use deltachat::constants::Chattype;
+use deltachat::chat::ChatVisibility;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download;
@@ -10,8 +10,7 @@ use deltachat::message::MsgId;
use deltachat::message::Viewtype;
use deltachat::reaction::get_msg_reactions;
use num_traits::cast::ToPrimitive;
-use serde::Deserialize;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
@@ -19,14 +18,14 @@ use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
-#[derive(Serialize, TypeDef)]
-#[serde(rename_all = "camelCase", tag = "variant")]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
+#[serde(rename_all = "camelCase", tag = "kind")]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },
}
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Message", rename_all = "camelCase")]
pub struct MessageObject {
id: u32,
@@ -35,7 +34,7 @@ pub struct MessageObject {
quote: Option,
parent_id: Option,
- text: Option,
+ text: String,
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
@@ -86,7 +85,7 @@ pub struct MessageObject {
reactions: Option,
}
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
enum MessageQuote {
JustText {
@@ -114,8 +113,12 @@ impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result {
let message = Message::load_from_db(context, msg_id).await?;
- let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
- let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
+ let sender_contact = Contact::get_by_id(context, message.get_from_id())
+ .await
+ .context("failed to load sender contact")?;
+ let sender = ContactObject::try_from_dc_contact(context, sender_contact)
+ .await
+ .context("failed to load sender contact object")?;
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
let override_sender_name = message.get_override_sender_name();
@@ -132,7 +135,9 @@ impl MessageObject {
let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? {
Some(quote) => {
- let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
+ let quote_author = Contact::get_by_id(context, quote.get_from_id())
+ .await
+ .context("failed to load quote author contact")?;
Some(MessageQuote::WithMessage {
text: quoted_text,
message_id: quote.get_id().to_u32(),
@@ -160,7 +165,9 @@ impl MessageObject {
None
};
- let reactions = get_msg_reactions(context, msg_id).await?;
+ let reactions = get_msg_reactions(context, msg_id)
+ .await
+ .context("failed to load message reactions")?;
let reactions = if reactions.is_empty() {
None
} else {
@@ -180,7 +187,7 @@ impl MessageObject {
state: message
.get_state()
.to_u32()
- .ok_or_else(|| anyhow!("state conversion to number failed"))?,
+ .context("state conversion to number failed")?,
error: message.error(),
timestamp: message.get_timestamp(),
@@ -203,7 +210,7 @@ impl MessageObject {
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
- .ok_or_else(|| anyhow!("state conversion to number failed"))?,
+ .context("videochat type conversion to number failed")?,
),
None => None,
},
@@ -230,7 +237,7 @@ impl MessageObject {
}
}
-#[derive(Serialize, Deserialize, TypeDef)]
+#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Viewtype")]
pub enum MessageViewtype {
Unknown,
@@ -306,11 +313,12 @@ impl From for Viewtype {
}
}
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
pub enum DownloadState {
Done,
Available,
Failure,
+ Undecipherable,
InProgress,
}
@@ -320,12 +328,13 @@ impl From for DownloadState {
download::DownloadState::Done => DownloadState::Done,
download::DownloadState::Available => DownloadState::Available,
download::DownloadState::Failure => DownloadState::Failure,
+ download::DownloadState::Undecipherable => DownloadState::Undecipherable,
download::DownloadState::InProgress => DownloadState::InProgress,
}
}
}
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
pub enum SystemMessageType {
Unknown,
GroupNameChanged,
@@ -380,7 +389,7 @@ impl From for SystemMessageType {
}
}
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageNotificationInfo {
id: u32,
@@ -438,14 +447,22 @@ impl MessageNotificationInfo {
}
}
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageSearchResult {
id: u32,
author_profile_image: Option,
+ /// if sender name if overridden it will show it as ~alias
author_name: String,
author_color: String,
- chat_name: Option,
+ author_id: u32,
+ chat_profile_image: Option,
+ chat_color: String,
+ chat_name: String,
+ chat_type: u32,
+ is_chat_protected: bool,
+ is_chat_contact_request: bool,
+ is_chat_archived: bool,
message: String,
timestamp: i64,
}
@@ -454,30 +471,44 @@ impl MessageSearchResult {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result {
let message = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
- let sender = Contact::load_from_db(context, message.get_from_id()).await?;
+ let sender = Contact::get_by_id(context, message.get_from_id()).await?;
let profile_image = match sender.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
+ let chat_profile_image = match chat.get_profile_image(context).await? {
+ Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
+ None => None,
+ };
+
+ let author_name = if let Some(name) = message.get_override_sender_name() {
+ format!("~{name}")
+ } else {
+ sender.get_display_name().to_owned()
+ };
+ let chat_color = color_int_to_hex_string(chat.get_color(context).await?);
Ok(Self {
id: msg_id.to_u32(),
author_profile_image: profile_image,
- author_name: sender.get_display_name().to_owned(),
+ author_name,
author_color: color_int_to_hex_string(sender.get_color()),
- chat_name: if chat.get_type() == Chattype::Single {
- Some(chat.get_name().to_owned())
- } else {
- None
- },
- message: message.get_text().unwrap_or_default(),
+ author_id: sender.id.to_u32(),
+ chat_name: chat.get_name().to_owned(),
+ chat_color,
+ chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
+ chat_profile_image,
+ is_chat_protected: chat.is_protected(),
+ is_chat_contact_request: chat.is_contact_request(),
+ is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
+ message: message.get_text(),
timestamp: message.get_timestamp(),
})
}
}
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
pub enum JSONRPCMessageListItem {
Message {
@@ -503,7 +534,7 @@ impl From for JSONRPCMessageListItem {
}
}
-#[derive(Deserialize, TypeDef)]
+#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageData {
pub text: Option,
@@ -514,3 +545,10 @@ pub struct MessageData {
pub override_sender_name: Option,
pub quoted_message_id: Option,
}
+
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct MessageReadReceipt {
+ pub contact_id: u32,
+ pub timestamp: i64,
+}
diff --git a/deltachat-jsonrpc/src/api/types/mod.rs b/deltachat-jsonrpc/src/api/types/mod.rs
index 2d783990e..8143be73d 100644
--- a/deltachat-jsonrpc/src/api/types/mod.rs
+++ b/deltachat-jsonrpc/src/api/types/mod.rs
@@ -2,6 +2,8 @@ pub mod account;
pub mod chat;
pub mod chat_list;
pub mod contact;
+pub mod events;
+pub mod http;
pub mod location;
pub mod message;
pub mod provider_info;
diff --git a/deltachat-jsonrpc/src/api/types/provider_info.rs b/deltachat-jsonrpc/src/api/types/provider_info.rs
index 1cc5a7d46..43b868444 100644
--- a/deltachat-jsonrpc/src/api/types/provider_info.rs
+++ b/deltachat-jsonrpc/src/api/types/provider_info.rs
@@ -3,7 +3,7 @@ use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderInfo {
pub before_login_hint: String,
diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs
index 607b495ec..0f6d79c8c 100644
--- a/deltachat-jsonrpc/src/api/types/qr.rs
+++ b/deltachat-jsonrpc/src/api/types/qr.rs
@@ -2,9 +2,9 @@ use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Qr", rename_all = "camelCase")]
-#[serde(tag = "type")]
+#[serde(tag = "kind")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
diff --git a/deltachat-jsonrpc/src/api/types/reactions.rs b/deltachat-jsonrpc/src/api/types/reactions.rs
index 8717ebdc2..37739c848 100644
--- a/deltachat-jsonrpc/src/api/types/reactions.rs
+++ b/deltachat-jsonrpc/src/api/types/reactions.rs
@@ -1,23 +1,37 @@
use std::collections::BTreeMap;
+use deltachat::contact::ContactId;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
+/// A single reaction emoji.
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
+#[serde(rename = "Reaction", rename_all = "camelCase")]
+pub struct JSONRPCReaction {
+ /// Emoji.
+ emoji: String,
+
+ /// Emoji frequency.
+ count: usize,
+
+ /// True if we reacted with this emoji.
+ is_from_self: bool,
+}
+
/// Structure representing all reactions to a particular message.
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap>,
- /// Unique reactions and their count
- reactions: BTreeMap,
+ /// Unique reactions and their count, sorted in descending order.
+ reactions: Vec,
}
impl From for JSONRPCReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap> = BTreeMap::new();
- let mut unique_reactions: BTreeMap = BTreeMap::new();
for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id);
@@ -30,18 +44,29 @@ impl From for JSONRPCReactions {
.map(|emoji| emoji.to_owned())
.collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
- for emoji in emojis {
- if let Some(x) = unique_reactions.get_mut(&emoji) {
- *x += 1;
- } else {
- unique_reactions.insert(emoji, 1);
- }
- }
+ }
+
+ let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
+
+ let mut reactions_v = Vec::new();
+ for (emoji, count) in reactions.emoji_sorted_by_frequency() {
+ let is_from_self = if let Some(self_reactions) = self_reactions {
+ self_reactions.contains(&emoji)
+ } else {
+ false
+ };
+
+ let reaction = JSONRPCReaction {
+ emoji,
+ count,
+ is_from_self,
+ };
+ reactions_v.push(reaction)
}
JSONRPCReactions {
reactions_by_contact,
- reactions: unique_reactions,
+ reactions: reactions_v,
}
}
}
diff --git a/deltachat-jsonrpc/src/api/types/webxdc.rs b/deltachat-jsonrpc/src/api/types/webxdc.rs
index f89310667..71db9ed93 100644
--- a/deltachat-jsonrpc/src/api/types/webxdc.rs
+++ b/deltachat-jsonrpc/src/api/types/webxdc.rs
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
use super::maybe_empty_string_to_option;
-#[derive(Serialize, TypeDef)]
+#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
pub struct WebxdcMessageInfo {
/// The name of the app.
diff --git a/deltachat-jsonrpc/src/lib.rs b/deltachat-jsonrpc/src/lib.rs
index bb4ea9cf6..10ec39ea4 100644
--- a/deltachat-jsonrpc/src/lib.rs
+++ b/deltachat-jsonrpc/src/lib.rs
@@ -1,5 +1,4 @@
pub mod api;
-pub use api::events;
pub use yerpc;
#[cfg(test)]
@@ -14,7 +13,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
- let accounts = Accounts::new(tmp_dir).await?;
+ let writable = true;
+ let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::();
@@ -55,7 +55,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn test_batch_set_config() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
- let accounts = Accounts::new(tmp_dir).await?;
+ let writable = true;
+ let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::();
diff --git a/deltachat-jsonrpc/src/webserver.rs b/deltachat-jsonrpc/src/webserver.rs
index 9231069c5..f4b6f38af 100644
--- a/deltachat-jsonrpc/src/webserver.rs
+++ b/deltachat-jsonrpc/src/webserver.rs
@@ -6,7 +6,6 @@ use yerpc::axum::handle_ws_rpc;
use yerpc::{RpcClient, RpcSession};
mod api;
-use api::events::event_to_json_rpc_notification;
use api::{Accounts, CommandApi};
const DEFAULT_PORT: u16 = 20808;
@@ -20,7 +19,8 @@ async fn main() -> Result<(), std::io::Error> {
.map(|port| port.parse::().expect("DC_PORT must be a number"))
.unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`.");
- let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
+ let writable = true;
+ let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
let state = CommandApi::new(accounts);
let app = Router::new()
@@ -44,12 +44,5 @@ async fn main() -> Result<(), std::io::Error> {
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension) -> Response {
let (client, out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), api.clone());
- tokio::spawn(async move {
- let events = api.accounts.read().await.get_event_emitter();
- while let Some(event) = events.recv().await {
- let event = event_to_json_rpc_notification(event);
- client.send_notification("event", Some(event)).await.ok();
- }
- });
handle_ws_rpc(ws, out_receiver, session).await
}
diff --git a/deltachat-jsonrpc/typescript/example/example.ts b/deltachat-jsonrpc/typescript/example/example.ts
index 0caa2de06..e45bc18cc 100644
--- a/deltachat-jsonrpc/typescript/example/example.ts
+++ b/deltachat-jsonrpc/typescript/example/example.ts
@@ -35,7 +35,7 @@ async function run() {
const accounts = await client.rpc.getAllAccounts();
console.log("accounts loaded", accounts);
for (const account of accounts) {
- if (account.type === "Configured") {
+ if (account.kind === "Configured") {
write(
$head,
`
@@ -57,7 +57,7 @@ async function run() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT;
const info = await client.rpc.getAccountInfo(selectedAccount);
- if (info.type !== "Configured") {
+ if (info.kind !== "Configured") {
return write($main, "Account is not configured");
}
write($main, `${info.addr!} `);
@@ -67,7 +67,7 @@ async function run() {
null,
null
);
- for (const [chatId, _messageId] of chats) {
+ for (const chatId of chats) {
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
write($main, `${chat.name} `);
const messageIds = await client.rpc.getMessageIds(
@@ -81,8 +81,7 @@ async function run() {
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
- if (message.variant === "message")
- write($main, `${message.text}
`);
+ if (message.kind === "message") write($main, `${message.text}
`);
else write($main, `loading error: ${message.error}
`);
}
}
@@ -93,9 +92,9 @@ async function run() {
$side,
`
- [${event.type} on account ${accountId}]
+ [${event.kind} on account ${accountId}]
f1: ${JSON.stringify(
- Object.assign({}, event, { type: undefined })
+ Object.assign({}, event, { kind: undefined })
)}
`
);
diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json
index e2743bba7..b32de8e51 100644
--- a/deltachat-jsonrpc/typescript/package.json
+++ b/deltachat-jsonrpc/typescript/package.json
@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
- "version": "1.112.6"
+ "version": "1.126.1"
}
diff --git a/deltachat-jsonrpc/typescript/src/client.ts b/deltachat-jsonrpc/typescript/src/client.ts
index 9efbab964..83cc2f7e7 100644
--- a/deltachat-jsonrpc/typescript/src/client.ts
+++ b/deltachat-jsonrpc/typescript/src/client.ts
@@ -1,34 +1,28 @@
import * as T from "../generated/types.js";
+import { EventType } from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
-import { Event } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
-type DCWireEvent = {
- event: T;
- contextId: number;
-};
-// export type Events = Record<
-// Event["type"] | "ALL",
-// (event: DeltaChatEvent) => void
-// >;
-
-type Events = { ALL: (accountId: number, event: Event) => void } & {
- [Property in Event["type"]]: (
+type Events = { ALL: (accountId: number, event: EventType) => void } & {
+ [Property in EventType["kind"]]: (
accountId: number,
- event: Extract
+ event: Extract
) => void;
};
-type ContextEvents = { ALL: (event: Event) => void } & {
- [Property in Event["type"]]: (
- event: Extract
+type ContextEvents = { ALL: (event: EventType) => void } & {
+ [Property in EventType["kind"]]: (
+ event: Extract
) => void;
};
-export type DcEvent = Event;
-export type DcEventType = Extract;
+export type DcEvent = EventType;
+export type DcEventType = Extract<
+ EventType,
+ { kind: T }
+>;
export class BaseDeltaChat<
Transport extends BaseTransport
@@ -36,27 +30,34 @@ export class BaseDeltaChat<
rpc: RawClient;
account?: T.Account;
private contextEmitters: { [key: number]: TinyEmitter } = {};
- constructor(public transport: Transport) {
+
+ //@ts-ignore
+ private eventTask: Promise;
+
+ constructor(public transport: Transport, startEventLoop: boolean) {
super();
this.rpc = new RawClient(this.transport);
- this.transport.on("request", (request: Request) => {
- const method = request.method;
- if (method === "event") {
- const event = request.params! as DCWireEvent;
- //@ts-ignore
- this.emit(event.event.type, event.contextId, event.event as any);
- this.emit("ALL", event.contextId, event.event as any);
+ if (startEventLoop) {
+ this.eventTask = this.eventLoop();
+ }
+ }
- if (this.contextEmitters[event.contextId]) {
- this.contextEmitters[event.contextId].emit(
- event.event.type,
- //@ts-ignore
- event.event as any
- );
- this.contextEmitters[event.contextId].emit("ALL", event.event);
- }
+ async eventLoop(): Promise {
+ while (true) {
+ const event = await this.rpc.getNextEvent();
+ //@ts-ignore
+ this.emit(event.event.kind, event.contextId, event.event);
+ this.emit("ALL", event.contextId, event.event);
+
+ if (this.contextEmitters[event.contextId]) {
+ this.contextEmitters[event.contextId].emit(
+ event.event.kind,
+ //@ts-ignore
+ event.event as any
+ );
+ this.contextEmitters[event.contextId].emit("ALL", event.event as any);
}
- });
+ }
}
async listAccounts(): Promise {
@@ -75,10 +76,12 @@ export class BaseDeltaChat<
export type Opts = {
url: string;
+ startEventLoop: boolean;
};
export const DEFAULT_OPTS: Opts = {
url: "ws://localhost:20808/ws",
+ startEventLoop: true,
};
export class DeltaChat extends BaseDeltaChat {
opts: Opts;
@@ -86,20 +89,24 @@ export class DeltaChat extends BaseDeltaChat {
this.transport.close();
}
constructor(opts?: Opts | string) {
- if (typeof opts === "string") opts = { url: opts };
- if (opts) opts = { ...DEFAULT_OPTS, ...opts };
- else opts = { ...DEFAULT_OPTS };
+ if (typeof opts === "string") {
+ opts = { ...DEFAULT_OPTS, url: opts };
+ } else if (opts) {
+ opts = { ...DEFAULT_OPTS, ...opts };
+ } else {
+ opts = { ...DEFAULT_OPTS };
+ }
const transport = new WebsocketTransport(opts.url);
- super(transport);
+ super(transport, opts.startEventLoop);
this.opts = opts;
}
}
export class StdioDeltaChat extends BaseDeltaChat {
close() {}
- constructor(input: any, output: any) {
+ constructor(input: any, output: any, startEventLoop: boolean) {
const transport = new StdioTransport(input, output);
- super(transport);
+ super(transport, startEventLoop);
}
}
diff --git a/deltachat-jsonrpc/typescript/src/lib.ts b/deltachat-jsonrpc/typescript/src/lib.ts
index 473d2bd33..de357a1ea 100644
--- a/deltachat-jsonrpc/typescript/src/lib.ts
+++ b/deltachat-jsonrpc/typescript/src/lib.ts
@@ -1,6 +1,5 @@
export * as RPC from "../generated/jsonrpc.js";
export * as T from "../generated/types.js";
-export * from "../generated/events.js";
export { RawClient } from "../generated/client.js";
export * from "./client.js";
export * as yerpc from "yerpc";
diff --git a/deltachat-jsonrpc/typescript/test/basic.ts b/deltachat-jsonrpc/typescript/test/basic.ts
index 1589a733d..1c430f3f5 100644
--- a/deltachat-jsonrpc/typescript/test/basic.ts
+++ b/deltachat-jsonrpc/typescript/test/basic.ts
@@ -12,7 +12,7 @@ describe("basic tests", () => {
before(async () => {
serverHandle = await startServer();
- dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
+ dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
// dc.on("ALL", (event) => {
//console.log("event", event);
// });
diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts
index 418f14679..c85376b09 100644
--- a/deltachat-jsonrpc/typescript/test/online.ts
+++ b/deltachat-jsonrpc/typescript/test/online.ts
@@ -27,10 +27,10 @@ describe("online tests", function () {
this.skip();
}
serverHandle = await startServer();
- dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
+ dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
- dc.on("ALL", (contextId, { type }) => {
- if (type !== "Info") console.log(contextId, type);
+ dc.on("ALL", (contextId, { kind }) => {
+ if (kind !== "Info") console.log(contextId, kind);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
@@ -148,7 +148,7 @@ describe("online tests", function () {
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
- // Check if answer arives at A and if it is encrypted
+ // Check if answer arrives at A and if it is encrypted
await eventPromise2;
const messageId = (
@@ -177,12 +177,12 @@ describe("online tests", function () {
});
});
-async function waitForEvent(
+async function waitForEvent(
dc: DeltaChat,
eventType: T,
accountId: number,
timeout: number = EVENT_TIMEOUT
-): Promise> {
+): Promise> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")),
diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml
index 3eca44d6a..dd5cb0a67 100644
--- a/deltachat-repl/Cargo.toml
+++ b/deltachat-repl/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
-version = "1.112.6"
+version = "1.126.1"
license = "MPL-2.0"
edition = "2021"
@@ -9,10 +9,10 @@ ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
-log = "0.4.16"
-pretty_env_logger = "0.4"
+log = "0.4.20"
+pretty_env_logger = "0.5"
rusqlite = "0.29"
-rustyline = "11"
+rustyline = "12"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features]
diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs
index dc4f75e12..40fa149fa 100644
--- a/deltachat-repl/src/cmdline.rs
+++ b/deltachat-repl/src/cmdline.rs
@@ -18,6 +18,7 @@ use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
+use deltachat::mimeparser::SystemMessage;
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
@@ -138,11 +139,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
/* import a directory */
let dir_name = std::path::Path::new(&real_spec);
let dir = fs::read_dir(dir_name).await;
- if dir.is_err() {
- error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
- return false;
- } else {
- let mut dir = dir.unwrap();
+ if let Ok(mut dir) = dir {
while let Ok(Some(entry)) = dir.next_entry().await {
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
@@ -154,6 +151,9 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
}
}
+ } else {
+ error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
+ return false;
}
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
@@ -187,6 +187,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) {
DownloadState::Available => " [⬇ Download available]",
DownloadState::InProgress => " [⬇ Download in progress...]️",
DownloadState::Failure => " [⬇ Download failed]",
+ DownloadState::Undecipherable => " [⬇ Decryption failed]",
};
let temp2 = timestamp_to_str(msg.get_timestamp());
@@ -199,7 +200,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) {
if msg.has_location() { "📍" } else { "" },
&contact_name,
contact_id,
- msgtext.unwrap_or_default(),
+ msgtext,
if msg.has_html() { "[HAS-HTML]️" } else { "" },
if msg.get_from_id() == ContactId::SELF {
""
@@ -210,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) {
} else {
"[FRESH]"
},
- if msg.is_info() { "[INFO]" } else { "" },
+ if msg.is_info() {
+ if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
+ "[INFO 🛡️]"
+ } else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
+ "[INFO 🛡️❌]"
+ } else {
+ "[INFO]"
+ }
+ } else {
+ ""
+ },
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
@@ -395,8 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unpin \n\
mute []\n\
unmute \n\
- protect \n\
- unprotect \n\
delchat \n\
accept \n\
decline \n\
@@ -805,15 +814,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"chatinfo" => {
ensure!(sel_chat.is_some(), "No chat selected.");
+ let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
- let contacts =
- chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
+ let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
println!("Memberlist:");
log_contactlist(&context, &contacts).await?;
+ println!("{} contacts", contacts.len());
+
+ let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
+ if !similar_chats.is_empty() {
+ println!("Similar chats: ");
+ for (similar_chat_id, metric) in similar_chats {
+ let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
+ println!(
+ "{} (#{}) {:.1}",
+ similar_chat.name,
+ similar_chat_id,
+ 100.0 * metric
+ );
+ }
+ }
+
println!(
- "{} contacts\nLocation streaming: {}",
- contacts.len(),
+ "Location streaming: {}",
location::is_sending_locations_to_chat(
&context,
Some(sel_chat.as_ref().unwrap().get_id())
@@ -878,7 +902,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
- let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
+ let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
if continue_streaming {
println!("Success, streaming should be continued.");
} else {
@@ -912,9 +936,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::File
});
msg.set_file(arg1, None);
- if !arg2.is_empty() {
- msg.set_text(Some(arg2.to_string()));
- }
+ msg.set_text(arg2.to_string());
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendhtml" => {
@@ -926,11 +948,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let mut msg = Message::new(Viewtype::Text);
msg.set_html(Some(html.to_string()));
- msg.set_text(Some(if arg2.is_empty() {
+ msg.set_text(if arg2.is_empty() {
path.file_name().unwrap().to_string_lossy().to_string()
} else {
arg2.to_string()
- }));
+ });
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendsyncmsg" => match context.send_sync_msg().await? {
@@ -979,7 +1001,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text);
- draft.set_text(Some(arg1.to_string()));
+ draft.set_text(arg1.to_string());
sel_chat
.as_ref()
.unwrap()
@@ -1003,7 +1025,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"Please specify text to add as device message."
);
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some(arg1.to_string()));
+ msg.set_text(arg1.to_string());
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
@@ -1058,20 +1080,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
};
chat::set_muted(&context, chat_id, duration).await?;
}
- "protect" | "unprotect" => {
- ensure!(!arg1.is_empty(), "Argument missing.");
- let chat_id = ChatId::new(arg1.parse()?);
- chat_id
- .set_protection(
- &context,
- match arg0 {
- "protect" => ProtectionStatus::Protected,
- "unprotect" => ProtectionStatus::Unprotected,
- _ => unreachable!("arg0={:?}", arg0),
- },
- )
- .await?;
- }
"delchat" => {
ensure!(!arg1.is_empty(), "Argument missing.");
let chat_id = ChatId::new(arg1.parse()?);
@@ -1090,7 +1098,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument missing.");
let id = MsgId::new(arg1.parse()?);
- let res = message::get_msg_info(&context, id).await?;
+ let res = id.get_info(&context).await?;
println!("{res}");
}
"download" => {
diff --git a/deltachat-rpc-client/LICENSE b/deltachat-rpc-client/LICENSE
new file mode 100644
index 000000000..d0a1fa148
--- /dev/null
+++ b/deltachat-rpc-client/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md
index 4ce7aa7c4..4b7503fce 100644
--- a/deltachat-rpc-client/README.md
+++ b/deltachat-rpc-client/README.md
@@ -5,9 +5,23 @@ and provides asynchronous interface to it.
## Getting started
-To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
+To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`
+or download a prebuilt release.
Install it anywhere in your `PATH`.
+[Create a virtual environment](https://docs.python.org/3/library/venv.html)
+if you don't have one already and activate it.
+```
+$ python -m venv env
+$ . env/bin/activate
+```
+
+Install `deltachat-rpc-client` from source:
+```
+$ cd deltachat-rpc-client
+$ pip install .
+```
+
## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
@@ -23,19 +37,14 @@ $ tox --devenv env
$ . env/bin/activate
```
-It is recommended to use IPython, because it supports using `await` directly
-from the REPL.
-
```
-$ pip install ipython
-$ PATH="../target/debug:$PATH" ipython
-...
-In [1]: from deltachat_rpc_client import *
-In [2]: rpc = Rpc()
-In [3]: await rpc.start()
-In [4]: dc = DeltaChat(rpc)
-In [5]: system_info = await dc.get_system_info()
-In [6]: system_info["level"]
-Out[6]: 'awesome'
-In [7]: await rpc.close()
+$ python
+>>> from deltachat_rpc_client import *
+>>> rpc = Rpc()
+>>> rpc.start()
+>>> dc = DeltaChat(rpc)
+>>> system_info = dc.get_system_info()
+>>> system_info["level"]
+'awesome'
+>>> rpc.close()
```
diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py
index 65d447bf9..7d2d79e8b 100755
--- a/deltachat-rpc-client/examples/echobot.py
+++ b/deltachat-rpc-client/examples/echobot.py
@@ -4,23 +4,21 @@
it will echo back any text send to it, it also will print to console all Delta Chat core events.
Pass --help to the CLI to see available options.
"""
-import asyncio
-
from deltachat_rpc_client import events, run_bot_cli
hooks = events.HookCollection()
@hooks.on(events.RawEvent)
-async def log_event(event):
+def log_event(event):
print(event)
@hooks.on(events.NewMessage)
-async def echo(event):
+def echo(event):
snapshot = event.message_snapshot
- await snapshot.chat.send_text(snapshot.text)
+ snapshot.chat.send_text(snapshot.text)
if __name__ == "__main__":
- asyncio.run(run_bot_cli(hooks))
+ run_bot_cli(hooks)
diff --git a/deltachat-rpc-client/examples/echobot_advanced.py b/deltachat-rpc-client/examples/echobot_advanced.py
index 3030941c8..221f69dda 100644
--- a/deltachat-rpc-client/examples/echobot_advanced.py
+++ b/deltachat-rpc-client/examples/echobot_advanced.py
@@ -3,9 +3,9 @@
it will echo back any message that has non-empty text and also supports the /help command.
"""
-import asyncio
import logging
import sys
+from threading import Thread
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
@@ -13,62 +13,62 @@ hooks = events.HookCollection()
@hooks.on(events.RawEvent)
-async def log_event(event):
- if event.type == EventType.INFO:
+def log_event(event):
+ if event.kind == EventType.INFO:
logging.info(event.msg)
- elif event.type == EventType.WARNING:
+ elif event.kind == EventType.WARNING:
logging.warning(event.msg)
@hooks.on(events.RawEvent(EventType.ERROR))
-async def log_error(event):
+def log_error(event):
logging.error(event.msg)
@hooks.on(events.MemberListChanged)
-async def on_memberlist_changed(event):
+def on_memberlist_changed(event):
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
@hooks.on(events.GroupImageChanged)
-async def on_group_image_changed(event):
+def on_group_image_changed(event):
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
@hooks.on(events.GroupNameChanged)
-async def on_group_name_changed(event):
+def on_group_name_changed(event):
logging.info("group name changed, old name: %s", event.old_name)
@hooks.on(events.NewMessage(func=lambda e: not e.command))
-async def echo(event):
+def echo(event):
snapshot = event.message_snapshot
if snapshot.text or snapshot.file:
- await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
+ snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
@hooks.on(events.NewMessage(command="/help"))
-async def help_command(event):
+def help_command(event):
snapshot = event.message_snapshot
- await snapshot.chat.send_text("Send me any message and I will echo it back")
+ snapshot.chat.send_text("Send me any message and I will echo it back")
-async def main():
- async with Rpc() as rpc:
+def main():
+ with Rpc() as rpc:
deltachat = DeltaChat(rpc)
- system_info = await deltachat.get_system_info()
+ system_info = deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
- accounts = await deltachat.get_all_accounts()
- account = accounts[0] if accounts else await deltachat.add_account()
+ accounts = deltachat.get_all_accounts()
+ account = accounts[0] if accounts else deltachat.add_account()
bot = Bot(account, hooks)
- if not await bot.is_configured():
- # Save a reference to avoid garbage collection of the task.
- _configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
- await bot.run_forever()
+ if not bot.is_configured():
+ configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
+ configure_thread.start()
+ bot.run_forever()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
- asyncio.run(main())
+ main()
diff --git a/deltachat-rpc-client/examples/echobot_no_hooks.py b/deltachat-rpc-client/examples/echobot_no_hooks.py
index 77fda86e7..73e875b0d 100644
--- a/deltachat-rpc-client/examples/echobot_no_hooks.py
+++ b/deltachat-rpc-client/examples/echobot_no_hooks.py
@@ -2,45 +2,44 @@
"""
Example echo bot without using hooks
"""
-import asyncio
import logging
import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
-async def main():
- async with Rpc() as rpc:
+def main():
+ with Rpc() as rpc:
deltachat = DeltaChat(rpc)
- system_info = await deltachat.get_system_info()
+ system_info = deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
- accounts = await deltachat.get_all_accounts()
- account = accounts[0] if accounts else await deltachat.add_account()
+ accounts = deltachat.get_all_accounts()
+ account = accounts[0] if accounts else deltachat.add_account()
- await account.set_config("bot", "1")
- if not await account.is_configured():
+ account.set_config("bot", "1")
+ if not account.is_configured():
logging.info("Account is not configured, configuring")
- await account.set_config("addr", sys.argv[1])
- await account.set_config("mail_pw", sys.argv[2])
- await account.configure()
+ account.set_config("addr", sys.argv[1])
+ account.set_config("mail_pw", sys.argv[2])
+ account.configure()
logging.info("Configured")
else:
logging.info("Account is already configured")
- await deltachat.start_io()
+ deltachat.start_io()
- async def process_messages():
- for message in await account.get_next_messages():
- snapshot = await message.get_snapshot()
+ def process_messages():
+ for message in account.get_next_messages():
+ snapshot = message.get_snapshot()
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
- await snapshot.chat.send_text(snapshot.text)
- await snapshot.message.mark_seen()
+ snapshot.chat.send_text(snapshot.text)
+ snapshot.message.mark_seen()
# Process old messages.
- await process_messages()
+ process_messages()
while True:
- event = await account.wait_for_event()
+ event = account.wait_for_event()
if event["type"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["type"] == EventType.WARNING:
@@ -49,9 +48,9 @@ async def main():
logging.error("%s", event["msg"])
elif event["type"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
- await process_messages()
+ process_messages()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
- asyncio.run(main())
+ main()
diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml
index b0d671df8..d0253741b 100644
--- a/deltachat-rpc-client/pyproject.toml
+++ b/deltachat-rpc-client/pyproject.toml
@@ -5,9 +5,19 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface"
-dependencies = [
- "aiohttp",
- "aiodns"
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
+ "Operating System :: POSIX :: Linux",
+ "Operating System :: MacOS :: MacOS X",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Topic :: Communications :: Chat",
+ "Topic :: Communications :: Email"
]
dynamic = [
"version"
@@ -61,3 +71,6 @@ line-length = 120
[tool.isort]
profile = "black"
+
+[tool.pytest.ini_options]
+log_cli = true
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py
index 727c51c80..6589813ae 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py
@@ -1,4 +1,4 @@
-"""Delta Chat asynchronous high-level API"""
+"""Delta Chat JSON-RPC high-level API"""
from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
from .chat import Chat
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py
index ec99f6dca..90a8b3d51 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py
@@ -1,7 +1,7 @@
import argparse
-import asyncio
import re
import sys
+from threading import Thread
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
if TYPE_CHECKING:
@@ -43,7 +43,7 @@ class AttrDict(dict):
super().__setattr__(attr, val)
-async def run_client_cli(
+def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs,
@@ -54,10 +54,10 @@ async def run_client_cli(
"""
from .client import Client
- await _run_cli(Client, hooks, argv, **kwargs)
+ _run_cli(Client, hooks, argv, **kwargs)
-async def run_bot_cli(
+def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs,
@@ -68,10 +68,10 @@ async def run_bot_cli(
"""
from .client import Bot
- await _run_cli(Bot, hooks, argv, **kwargs)
+ _run_cli(Bot, hooks, argv, **kwargs)
-async def _run_cli(
+def _run_cli(
client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
@@ -93,20 +93,20 @@ async def _run_cli(
parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:])
- async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
+ with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
deltachat = DeltaChat(rpc)
- core_version = (await deltachat.get_system_info()).deltachat_core_version
- accounts = await deltachat.get_all_accounts()
- account = accounts[0] if accounts else await deltachat.add_account()
+ core_version = (deltachat.get_system_info()).deltachat_core_version
+ accounts = deltachat.get_all_accounts()
+ account = accounts[0] if accounts else deltachat.add_account()
client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version)
- if not await client.is_configured():
+ if not client.is_configured():
assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided"
- # Save a reference to avoid garbage collection of the task.
- _configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
- await client.run_forever()
+ configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
+ configure_thread.start()
+ client.run_forever()
def extract_addr(text: str) -> str:
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py
index 4c44079f7..0c6c53a2a 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py
@@ -24,63 +24,63 @@ class Account:
def _rpc(self) -> "Rpc":
return self.manager.rpc
- async def wait_for_event(self) -> AttrDict:
+ def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
- return AttrDict(await self._rpc.wait_for_event(self.id))
+ return AttrDict(self._rpc.wait_for_event(self.id))
- async def remove(self) -> None:
+ def remove(self) -> None:
"""Remove the account."""
- await self._rpc.remove_account(self.id)
+ self._rpc.remove_account(self.id)
- async def start_io(self) -> None:
+ def start_io(self) -> None:
"""Start the account I/O."""
- await self._rpc.start_io(self.id)
+ self._rpc.start_io(self.id)
- async def stop_io(self) -> None:
+ def stop_io(self) -> None:
"""Stop the account I/O."""
- await self._rpc.stop_io(self.id)
+ self._rpc.stop_io(self.id)
- async def get_info(self) -> AttrDict:
+ def get_info(self) -> AttrDict:
"""Return dictionary of this account configuration parameters."""
- return AttrDict(await self._rpc.get_info(self.id))
+ return AttrDict(self._rpc.get_info(self.id))
- async def get_size(self) -> int:
+ def get_size(self) -> int:
"""Get the combined filesize of an account in bytes."""
- return await self._rpc.get_account_file_size(self.id)
+ return self._rpc.get_account_file_size(self.id)
- async def is_configured(self) -> bool:
+ def is_configured(self) -> bool:
"""Return True if this account is configured."""
- return await self._rpc.is_configured(self.id)
+ return self._rpc.is_configured(self.id)
- async def set_config(self, key: str, value: Optional[str] = None) -> None:
+ def set_config(self, key: str, value: Optional[str] = None) -> None:
"""Set configuration value."""
- await self._rpc.set_config(self.id, key, value)
+ self._rpc.set_config(self.id, key, value)
- async def get_config(self, key: str) -> Optional[str]:
+ def get_config(self, key: str) -> Optional[str]:
"""Get configuration value."""
- return await self._rpc.get_config(self.id, key)
+ return self._rpc.get_config(self.id, key)
- async def update_config(self, **kwargs) -> None:
+ def update_config(self, **kwargs) -> None:
"""update config values."""
for key, value in kwargs.items():
- await self.set_config(key, value)
+ self.set_config(key, value)
- async def set_avatar(self, img_path: Optional[str] = None) -> None:
+ def set_avatar(self, img_path: Optional[str] = None) -> None:
"""Set self avatar.
Passing None will discard the currently set avatar.
"""
- await self.set_config("selfavatar", img_path)
+ self.set_config("selfavatar", img_path)
- async def get_avatar(self) -> Optional[str]:
+ def get_avatar(self) -> Optional[str]:
"""Get self avatar."""
- return await self.get_config("selfavatar")
+ return self.get_config("selfavatar")
- async def configure(self) -> None:
+ def configure(self) -> None:
"""Configure an account."""
- await self._rpc.configure(self.id)
+ self._rpc.configure(self.id)
- async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
+ def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
Calling this method will always result in the same
@@ -94,24 +94,24 @@ class Account:
if isinstance(obj, int):
obj = Contact(self, obj)
if isinstance(obj, Contact):
- obj = (await obj.get_snapshot()).address
- return Contact(self, await self._rpc.create_contact(self.id, obj, name))
+ obj = obj.get_snapshot().address
+ return Contact(self, self._rpc.create_contact(self.id, obj, name))
def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID."""
return Contact(self, contact_id)
- async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
+ def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Check if an e-mail address belongs to a known and unblocked contact."""
- contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address)
+ contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
- async def get_blocked_contacts(self) -> List[AttrDict]:
+ def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts."""
- contacts = await self._rpc.get_blocked_contacts(self.id)
+ contacts = self._rpc.get_blocked_contacts(self.id)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
- async def get_contacts(
+ def get_contacts(
self,
query: Optional[str] = None,
with_self: bool = False,
@@ -133,9 +133,9 @@ class Account:
flags |= ContactFlag.ADD_SELF
if snapshot:
- contacts = await self._rpc.get_contacts(self.id, flags, query)
+ contacts = self._rpc.get_contacts(self.id, flags, query)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
- contacts = await self._rpc.get_contact_ids(self.id, flags, query)
+ contacts = self._rpc.get_contact_ids(self.id, flags, query)
return [Contact(self, contact_id) for contact_id in contacts]
@property
@@ -143,7 +143,7 @@ class Account:
"""This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
- async def get_chatlist(
+ def get_chatlist(
self,
query: Optional[str] = None,
contact: Optional[Contact] = None,
@@ -175,29 +175,29 @@ class Account:
if alldone_hint:
flags |= ChatlistFlag.ADD_ALLDONE_HINT
- entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
+ entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
if not snapshot:
- return [Chat(self, entry[0]) for entry in entries]
+ return [Chat(self, entry) for entry in entries]
- items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
+ items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
chats = []
for item in items.values():
item["chat"] = Chat(self, item["id"])
chats.append(AttrDict(item))
return chats
- async def create_group(self, name: str, protect: bool = False) -> Chat:
+ def create_group(self, name: str, protect: bool = False) -> Chat:
"""Create a new group chat.
After creation, the group has only self-contact as member and is in unpromoted state.
"""
- return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
+ return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID."""
return Chat(self, chat_id)
- async def secure_join(self, qrdata: str) -> Chat:
+ def secure_join(self, qrdata: str) -> Chat:
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device.
@@ -208,54 +208,62 @@ class Account:
:param qrdata: The text of the scanned QR code.
"""
- return Chat(self, await self._rpc.secure_join(self.id, qrdata))
+ return Chat(self, self._rpc.secure_join(self.id, qrdata))
- async def get_qr_code(self) -> Tuple[str, str]:
+ def get_qr_code(self) -> Tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data.
this data needs to be transferred to another Delta Chat account
in a second channel, typically used by mobiles with QRcode-show + scan UX.
"""
- return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
+ return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
def get_message_by_id(self, msg_id: int) -> Message:
"""Return the Message instance with the given ID."""
return Message(self, msg_id)
- async def mark_seen_messages(self, messages: List[Message]) -> None:
+ def mark_seen_messages(self, messages: List[Message]) -> None:
"""Mark the given set of messages as seen."""
- await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
+ self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
- async def delete_messages(self, messages: List[Message]) -> None:
+ def delete_messages(self, messages: List[Message]) -> None:
"""Delete messages (local and remote)."""
- await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
+ self._rpc.delete_messages(self.id, [msg.id for msg in messages])
- async def get_fresh_messages(self) -> List[Message]:
+ def get_fresh_messages(self) -> List[Message]:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
to process oldest messages first.
"""
- fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
+ fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
- async def get_next_messages(self) -> List[Message]:
+ def get_next_messages(self) -> List[Message]:
"""Return a list of next messages."""
- next_msg_ids = await self._rpc.get_next_msgs(self.id)
+ next_msg_ids = self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
- async def wait_next_messages(self) -> List[Message]:
+ def wait_next_messages(self) -> List[Message]:
"""Wait for new messages and return a list of them."""
- next_msg_ids = await self._rpc.wait_next_msgs(self.id)
+ next_msg_ids = self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
- async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
+ def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning,
stacklevel=2,
)
- fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
+ fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
+
+ def export_backup(self, path, passphrase: str = "") -> None:
+ """Export backup."""
+ self._rpc.export_backup(self.id, str(path), passphrase)
+
+ def import_backup(self, path, passphrase: str = "") -> None:
+ """Import backup."""
+ self._rpc.import_backup(self.id, str(path), passphrase)
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py
index a9cfad8ab..20fc11b36 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py
@@ -25,7 +25,7 @@ class Chat:
def _rpc(self) -> "Rpc":
return self.account._rpc
- async def delete(self) -> None:
+ def delete(self) -> None:
"""Delete this chat and all its messages.
Note:
@@ -33,83 +33,83 @@ class Chat:
- does not delete messages on server
- the chat or contact is not blocked, new message will arrive
"""
- await self._rpc.delete_chat(self.account.id, self.id)
+ self._rpc.delete_chat(self.account.id, self.id)
- async def block(self) -> None:
+ def block(self) -> None:
"""Block this chat."""
- await self._rpc.block_chat(self.account.id, self.id)
+ self._rpc.block_chat(self.account.id, self.id)
- async def accept(self) -> None:
+ def accept(self) -> None:
"""Accept this contact request chat."""
- await self._rpc.accept_chat(self.account.id, self.id)
+ self._rpc.accept_chat(self.account.id, self.id)
- async def leave(self) -> None:
+ def leave(self) -> None:
"""Leave this chat."""
- await self._rpc.leave_group(self.account.id, self.id)
+ self._rpc.leave_group(self.account.id, self.id)
- async def mute(self, duration: Optional[int] = None) -> None:
+ def mute(self, duration: Optional[int] = None) -> None:
"""Mute this chat, if a duration is not provided the chat is muted forever.
:param duration: mute duration from now in seconds. Must be greater than zero.
"""
if duration is not None:
assert duration > 0, "Invalid duration"
- dur: Union[str, dict] = {"Until": duration}
+ dur: dict = {"kind": "Until", "duration": duration}
else:
- dur = "Forever"
- await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
+ dur = {"kind": "Forever"}
+ self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
- async def unmute(self) -> None:
+ def unmute(self) -> None:
"""Unmute this chat."""
- await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
+ self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
- async def pin(self) -> None:
+ def pin(self) -> None:
"""Pin this chat."""
- await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
+ self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
- async def unpin(self) -> None:
+ def unpin(self) -> None:
"""Unpin this chat."""
- await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
+ self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
- async def archive(self) -> None:
+ def archive(self) -> None:
"""Archive this chat."""
- await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
+ self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
- async def unarchive(self) -> None:
+ def unarchive(self) -> None:
"""Unarchive this chat."""
- await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
+ self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
- async def set_name(self, name: str) -> None:
+ def set_name(self, name: str) -> None:
"""Set name of this chat."""
- await self._rpc.set_chat_name(self.account.id, self.id, name)
+ self._rpc.set_chat_name(self.account.id, self.id, name)
- async def set_ephemeral_timer(self, timer: int) -> None:
+ def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat."""
- await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
+ self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
- async def get_encryption_info(self) -> str:
+ def get_encryption_info(self) -> str:
"""Return encryption info for this chat."""
- return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
+ return self._rpc.get_chat_encryption_info(self.account.id, self.id)
- async def get_qr_code(self) -> Tuple[str, str]:
+ def get_qr_code(self) -> Tuple[str, str]:
"""Get Join-Group QR code text and SVG data."""
- return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
+ return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
- async def get_basic_snapshot(self) -> AttrDict:
+ def get_basic_snapshot(self) -> AttrDict:
"""Get a chat snapshot with basic info about this chat."""
- info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
+ info = self._rpc.get_basic_chat_info(self.account.id, self.id)
return AttrDict(chat=self, **info)
- async def get_full_snapshot(self) -> AttrDict:
+ def get_full_snapshot(self) -> AttrDict:
"""Get a full snapshot of this chat."""
- info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
+ info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
return AttrDict(chat=self, **info)
- async def can_send(self) -> bool:
+ def can_send(self) -> bool:
"""Return true if messages can be sent to the chat."""
- return await self._rpc.can_send(self.account.id, self.id)
+ return self._rpc.can_send(self.account.id, self.id)
- async def send_message(
+ def send_message(
self,
text: Optional[str] = None,
html: Optional[str] = None,
@@ -132,30 +132,30 @@ class Chat:
"overrideSenderName": override_sender_name,
"quotedMessageId": quoted_msg,
}
- msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
+ msg_id = self._rpc.send_msg(self.account.id, self.id, draft)
return Message(self.account, msg_id)
- async def send_text(self, text: str) -> Message:
+ def send_text(self, text: str) -> Message:
"""Send a text message and return the resulting Message instance."""
- msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
+ msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
return Message(self.account, msg_id)
- async def send_videochat_invitation(self) -> Message:
+ def send_videochat_invitation(self) -> Message:
"""Send a videochat invitation and return the resulting Message instance."""
- msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
+ msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
return Message(self.account, msg_id)
- async def send_sticker(self, path: str) -> Message:
+ def send_sticker(self, path: str) -> Message:
"""Send an sticker and return the resulting Message instance."""
- msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
+ msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
- async def forward_messages(self, messages: List[Message]) -> None:
+ def forward_messages(self, messages: List[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
- await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
+ self._rpc.forward_messages(self.account.id, msg_ids, self.id)
- async def set_draft(
+ def set_draft(
self,
text: Optional[str] = None,
file: Optional[str] = None,
@@ -164,15 +164,15 @@ class Chat:
"""Set draft message."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
- await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
+ self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
- async def remove_draft(self) -> None:
+ def remove_draft(self) -> None:
"""Remove draft message."""
- await self._rpc.remove_draft(self.account.id, self.id)
+ self._rpc.remove_draft(self.account.id, self.id)
- async def get_draft(self) -> Optional[AttrDict]:
+ def get_draft(self) -> Optional[AttrDict]:
"""Get draft message."""
- snapshot = await self._rpc.get_draft(self.account.id, self.id)
+ snapshot = self._rpc.get_draft(self.account.id, self.id)
if not snapshot:
return None
snapshot = AttrDict(snapshot)
@@ -181,61 +181,61 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
- async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
+ def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
"""get the list of messages in this chat."""
- msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
+ msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
- async def get_fresh_message_count(self) -> int:
+ def get_fresh_message_count(self) -> int:
"""Get number of fresh messages in this chat"""
- return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
+ return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
- async def mark_noticed(self) -> None:
+ def mark_noticed(self) -> None:
"""Mark all messages in this chat as noticed."""
- await self._rpc.marknoticed_chat(self.account.id, self.id)
+ self._rpc.marknoticed_chat(self.account.id, self.id)
- async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
+ def add_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Add contacts to this group."""
for cnt in contact:
if isinstance(cnt, str):
- contact_id = (await self.account.create_contact(cnt)).id
+ contact_id = self.account.create_contact(cnt).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
- await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
+ self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
- async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
+ def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group."""
for cnt in contact:
if isinstance(cnt, str):
- contact_id = (await self.account.create_contact(cnt)).id
+ contact_id = self.account.create_contact(cnt).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
- await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
+ self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
- async def get_contacts(self) -> List[Contact]:
+ def get_contacts(self) -> List[Contact]:
"""Get the contacts belonging to this chat.
For single/direct chats self-address is not included.
"""
- contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
+ contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts]
- async def set_image(self, path: str) -> None:
+ def set_image(self, path: str) -> None:
"""Set profile image of this chat.
:param path: Full path of the image to use as the group image.
"""
- await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
+ self._rpc.set_chat_profile_image(self.account.id, self.id, path)
- async def remove_image(self) -> None:
+ def remove_image(self) -> None:
"""Remove profile image of this chat."""
- await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
+ self._rpc.set_chat_profile_image(self.account.id, self.id, None)
- async def get_locations(
+ def get_locations(
self,
contact: Optional[Contact] = None,
timestamp_from: Optional["datetime"] = None,
@@ -246,7 +246,7 @@ class Chat:
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
contact_id = contact.id if contact else 0
- result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
+ result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
locations = []
contacts: Dict[int, Contact] = {}
for loc in result:
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py
index 5205a1ed9..31b628ffa 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py
@@ -1,10 +1,8 @@
"""Event loop implementations offering high level event handling/hooking."""
-import inspect
import logging
from typing import (
TYPE_CHECKING,
Callable,
- Coroutine,
Dict,
Iterable,
Optional,
@@ -78,22 +76,22 @@ class Client:
)
self._hooks.get(type(event), set()).remove((hook, event))
- async def is_configured(self) -> bool:
- return await self.account.is_configured()
+ def is_configured(self) -> bool:
+ return self.account.is_configured()
- async def configure(self, email: str, password: str, **kwargs) -> None:
- await self.account.set_config("addr", email)
- await self.account.set_config("mail_pw", password)
+ def configure(self, email: str, password: str, **kwargs) -> None:
+ self.account.set_config("addr", email)
+ self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
- await self.account.set_config(key, value)
- await self.account.configure()
+ self.account.set_config(key, value)
+ self.account.configure()
self.logger.debug("Account configured")
- async def run_forever(self) -> None:
+ def run_forever(self) -> None:
"""Process events forever."""
- await self.run_until(lambda _: False)
+ self.run_until(lambda _: False)
- async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
+ def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
"""Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the
@@ -101,39 +99,37 @@ class Client:
evaluates to True.
"""
self.logger.debug("Listening to incoming events...")
- if await self.is_configured():
- await self.account.start_io()
- await self._process_messages() # Process old messages.
+ if self.is_configured():
+ self.account.start_io()
+ self._process_messages() # Process old messages.
while True:
- event = await self.account.wait_for_event()
- event["type"] = EventType(event.type)
+ event = self.account.wait_for_event()
+ event["kind"] = EventType(event.kind)
event["account"] = self.account
- await self._on_event(event)
- if event.type == EventType.INCOMING_MSG:
- await self._process_messages()
+ self._on_event(event)
+ if event.kind == EventType.INCOMING_MSG:
+ self._process_messages()
stop = func(event)
- if inspect.isawaitable(stop):
- stop = await stop
if stop:
return event
- async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
+ def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
for hook, evfilter in self._hooks.get(filter_type, []):
- if await evfilter.filter(event):
+ if evfilter.filter(event):
try:
- await hook(event)
+ hook(event)
except Exception as ex:
self.logger.exception(ex)
- async def _parse_command(self, event: AttrDict) -> None:
+ def _parse_command(self, event: AttrDict) -> None:
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
parts = event.message_snapshot.text.split(maxsplit=1)
payload = parts[1] if len(parts) > 1 else ""
cmd = parts.pop(0)
if "@" in cmd:
- suffix = "@" + (await self.account.self_contact.get_snapshot()).address
+ suffix = "@" + self.account.self_contact.get_snapshot().address
if cmd.endswith(suffix):
cmd = cmd[: -len(suffix)]
else:
@@ -153,32 +149,32 @@ class Client:
event["command"], event["payload"] = cmd, payload
- async def _on_new_msg(self, snapshot: AttrDict) -> None:
+ def _on_new_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(command="", payload="", message_snapshot=snapshot)
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
- await self._parse_command(event)
- await self._on_event(event, NewMessage)
+ self._parse_command(event)
+ self._on_event(event, NewMessage)
- async def _handle_info_msg(self, snapshot: AttrDict) -> None:
+ def _handle_info_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(message_snapshot=snapshot)
img_changed = parse_system_image_changed(snapshot.text)
if img_changed:
_, event["image_deleted"] = img_changed
- await self._on_event(event, GroupImageChanged)
+ self._on_event(event, GroupImageChanged)
return
title_changed = parse_system_title_changed(snapshot.text)
if title_changed:
_, event["old_name"] = title_changed
- await self._on_event(event, GroupNameChanged)
+ self._on_event(event, GroupNameChanged)
return
members_changed = parse_system_add_remove(snapshot.text)
if members_changed:
action, event["member"], _ = members_changed
event["member_added"] = action == "added"
- await self._on_event(event, MemberListChanged)
+ self._on_event(event, MemberListChanged)
return
self.logger.warning(
@@ -187,20 +183,20 @@ class Client:
snapshot.text,
)
- async def _process_messages(self) -> None:
+ def _process_messages(self) -> None:
if self._should_process_messages:
- for message in await self.account.get_next_messages():
- snapshot = await message.get_snapshot()
+ for message in self.account.get_next_messages():
+ snapshot = message.get_snapshot()
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
- await self._on_new_msg(snapshot)
+ self._on_new_msg(snapshot)
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
- await self._handle_info_msg(snapshot)
- await snapshot.message.mark_seen()
+ self._handle_info_msg(snapshot)
+ snapshot.message.mark_seen()
class Bot(Client):
"""Simple bot implementation that listent to events of a single account."""
- async def configure(self, email: str, password: str, **kwargs) -> None:
+ def configure(self, email: str, password: str, **kwargs) -> None:
kwargs.setdefault("bot", "1")
- await super().configure(email, password, **kwargs)
+ super().configure(email, password, **kwargs)
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/const.py b/deltachat-rpc-client/src/deltachat_rpc_client/const.py
index 3ca606617..c17ca8637 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/const.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/const.py
@@ -45,6 +45,7 @@ class EventType(str, Enum):
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"
MSG_READ = "MsgRead"
+ MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
index efb3e9297..8f3c09d7f 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
@@ -24,39 +24,39 @@ class Contact:
def _rpc(self) -> "Rpc":
return self.account._rpc
- async def block(self) -> None:
+ def block(self) -> None:
"""Block contact."""
- await self._rpc.block_contact(self.account.id, self.id)
+ self._rpc.block_contact(self.account.id, self.id)
- async def unblock(self) -> None:
+ def unblock(self) -> None:
"""Unblock contact."""
- await self._rpc.unblock_contact(self.account.id, self.id)
+ self._rpc.unblock_contact(self.account.id, self.id)
- async def delete(self) -> None:
+ def delete(self) -> None:
"""Delete contact."""
- await self._rpc.delete_contact(self.account.id, self.id)
+ self._rpc.delete_contact(self.account.id, self.id)
- async def set_name(self, name: str) -> None:
+ def set_name(self, name: str) -> None:
"""Change the name of this contact."""
- await self._rpc.change_contact_name(self.account.id, self.id, name)
+ self._rpc.change_contact_name(self.account.id, self.id, name)
- async def get_encryption_info(self) -> str:
+ def get_encryption_info(self) -> str:
"""Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact.
"""
- return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
+ return self._rpc.get_contact_encryption_info(self.account.id, self.id)
- async def get_snapshot(self) -> AttrDict:
+ def get_snapshot(self) -> AttrDict:
"""Return a dictionary with a snapshot of all contact properties."""
- snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
+ snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id))
snapshot["contact"] = self
return snapshot
- async def create_chat(self) -> "Chat":
+ def create_chat(self) -> "Chat":
"""Create or get an existing 1:1 chat for this contact."""
from .chat import Chat
return Chat(
self.account,
- await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
+ self._rpc.create_chat_by_contact_id(self.account.id, self.id),
)
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
index c2cecd60d..ec3ed2d76 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
@@ -16,34 +16,34 @@ class DeltaChat:
def __init__(self, rpc: "Rpc") -> None:
self.rpc = rpc
- async def add_account(self) -> Account:
+ def add_account(self) -> Account:
"""Create a new account database."""
- account_id = await self.rpc.add_account()
+ account_id = self.rpc.add_account()
return Account(self, account_id)
- async def get_all_accounts(self) -> List[Account]:
+ def get_all_accounts(self) -> List[Account]:
"""Return a list of all available accounts."""
- account_ids = await self.rpc.get_all_account_ids()
+ account_ids = self.rpc.get_all_account_ids()
return [Account(self, account_id) for account_id in account_ids]
- async def start_io(self) -> None:
+ def start_io(self) -> None:
"""Start the I/O of all accounts."""
- await self.rpc.start_io_for_all_accounts()
+ self.rpc.start_io_for_all_accounts()
- async def stop_io(self) -> None:
+ def stop_io(self) -> None:
"""Stop the I/O of all accounts."""
- await self.rpc.stop_io_for_all_accounts()
+ self.rpc.stop_io_for_all_accounts()
- async def maybe_network(self) -> None:
+ def maybe_network(self) -> None:
"""Indicate that the network likely has come back or just that the network
conditions might have changed.
"""
- await self.rpc.maybe_network()
+ self.rpc.maybe_network()
- async def get_system_info(self) -> AttrDict:
+ def get_system_info(self) -> AttrDict:
"""Get information about the Delta Chat core in this system."""
- return AttrDict(await self.rpc.get_system_info())
+ return AttrDict(self.rpc.get_system_info())
- async def set_translations(self, translations: Dict[str, str]) -> None:
+ def set_translations(self, translations: Dict[str, str]) -> None:
"""Set stock translation strings."""
- await self.rpc.set_stock_strings(translations)
+ self.rpc.set_stock_strings(translations)
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py
index 4896527b9..b90b6e045 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py
@@ -1,5 +1,4 @@
"""High-level classes for event processing and filtering."""
-import inspect
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
@@ -24,7 +23,7 @@ def _tuple_of(obj, type_: type) -> tuple:
class EventFilter(ABC):
"""The base event filter.
- :param func: A Callable (async or not) function that should accept the event as input
+ :param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -43,16 +42,13 @@ class EventFilter(ABC):
def __ne__(self, other):
return not self == other
- async def _call_func(self, event) -> bool:
+ def _call_func(self, event) -> bool:
if not self.func:
return True
- res = self.func(event)
- if inspect.isawaitable(res):
- return await res
- return res
+ return self.func(event)
@abstractmethod
- async def filter(self, event):
+ def filter(self, event):
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
@@ -62,7 +58,7 @@ class RawEvent(EventFilter):
"""Matches raw core events.
:param types: The types of event to match.
- :param func: A Callable (async or not) function that should accept the event as input
+ :param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -82,10 +78,10 @@ class RawEvent(EventFilter):
return (self.types, self.func) == (other.types, other.func)
return False
- async def filter(self, event: "AttrDict") -> bool:
- if self.types and event.type not in self.types:
+ def filter(self, event: "AttrDict") -> bool:
+ if self.types and event.kind not in self.types:
return False
- return await self._call_func(event)
+ return self._call_func(event)
class NewMessage(EventFilter):
@@ -104,7 +100,7 @@ class NewMessage(EventFilter):
:param is_info: If set to True only match info/system messages, if set to False
only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched.
- :param func: A Callable (async or not) function that should accept the event as input
+ :param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -159,7 +155,7 @@ class NewMessage(EventFilter):
)
return False
- async def filter(self, event: "AttrDict") -> bool:
+ def filter(self, event: "AttrDict") -> bool:
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
@@ -168,11 +164,9 @@ class NewMessage(EventFilter):
return False
if self.pattern:
match = self.pattern(event.message_snapshot.text)
- if inspect.isawaitable(match):
- match = await match
if not match:
return False
- return await super()._call_func(event)
+ return super()._call_func(event)
class MemberListChanged(EventFilter):
@@ -184,7 +178,7 @@ class MemberListChanged(EventFilter):
:param added: If set to True only match if a member was added, if set to False
only match if a member was removed. If omitted both, member additions
and removals, will be matched.
- :param func: A Callable (async or not) function that should accept the event as input
+ :param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -201,10 +195,10 @@ class MemberListChanged(EventFilter):
return (self.added, self.func) == (other.added, other.func)
return False
- async def filter(self, event: "AttrDict") -> bool:
+ def filter(self, event: "AttrDict") -> bool:
if self.added is not None and self.added != event.member_added:
return False
- return await self._call_func(event)
+ return self._call_func(event)
class GroupImageChanged(EventFilter):
@@ -216,7 +210,7 @@ class GroupImageChanged(EventFilter):
:param deleted: If set to True only match if the image was deleted, if set to False
only match if a new image was set. If omitted both, image changes and
removals, will be matched.
- :param func: A Callable (async or not) function that should accept the event as input
+ :param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -233,10 +227,10 @@ class GroupImageChanged(EventFilter):
return (self.deleted, self.func) == (other.deleted, other.func)
return False
- async def filter(self, event: "AttrDict") -> bool:
+ def filter(self, event: "AttrDict") -> bool:
if self.deleted is not None and self.deleted != event.image_deleted:
return False
- return await self._call_func(event)
+ return self._call_func(event)
class GroupNameChanged(EventFilter):
@@ -245,7 +239,7 @@ class GroupNameChanged(EventFilter):
Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots.
- :param func: A Callable (async or not) function that should accept the event as input
+ :param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -258,8 +252,8 @@ class GroupNameChanged(EventFilter):
return self.func == other.func
return False
- async def filter(self, event: "AttrDict") -> bool:
- return await self._call_func(event)
+ def filter(self, event: "AttrDict") -> bool:
+ return self._call_func(event)
class HookCollection:
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py
index 5ec30961a..e728e690f 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py
@@ -1,6 +1,6 @@
import json
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Union
+from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict
from .contact import Contact
@@ -21,32 +21,39 @@ class Message:
def _rpc(self) -> "Rpc":
return self.account._rpc
- async def send_reaction(self, *reaction: str):
+ def send_reaction(self, *reaction: str):
"""Send a reaction to this message."""
- await self._rpc.send_reaction(self.account.id, self.id, reaction)
+ self._rpc.send_reaction(self.account.id, self.id, reaction)
- async def get_snapshot(self) -> AttrDict:
+ def get_snapshot(self) -> AttrDict:
"""Get a snapshot with the properties of this message."""
from .chat import Chat
- snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
+ snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = self
return snapshot
- async def mark_seen(self) -> None:
- """Mark the message as seen."""
- await self._rpc.markseen_msgs(self.account.id, [self.id])
+ def get_reactions(self) -> Optional[AttrDict]:
+ """Get message reactions."""
+ reactions = self._rpc.get_message_reactions(self.account.id, self.id)
+ if reactions:
+ return AttrDict(reactions)
+ return None
- async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
+ def mark_seen(self) -> None:
+ """Mark the message as seen."""
+ self._rpc.markseen_msgs(self.account.id, [self.id])
+
+ def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
"""Send a webxdc status update. This message must be a webxdc."""
if not isinstance(update, str):
update = json.dumps(update)
- await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
+ self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
- async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
- return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
+ def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
+ return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
- async def get_webxdc_info(self) -> dict:
- return await self._rpc.get_webxdc_info(self.account.id, self.id)
+ def get_webxdc_info(self) -> dict:
+ return self._rpc.get_webxdc_info(self.account.id, self.id)
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py
index 86ae86afd..4d569d3ea 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py
@@ -1,71 +1,67 @@
-import asyncio
import json
import os
+import urllib.request
from typing import AsyncGenerator, List, Optional
-import aiohttp
-import pytest_asyncio
+import pytest
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from .rpc import Rpc
-async def get_temp_credentials() -> dict:
+def get_temp_credentials() -> dict:
url = os.getenv("DCC_NEW_TMP_EMAIL")
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
- # Replace default 5 minute timeout with a 1 minute timeout.
- timeout = aiohttp.ClientTimeout(total=60)
- async with aiohttp.ClientSession() as session:
- async with session.post(url, timeout=timeout) as response:
- return json.loads(await response.text())
+ request = urllib.request.Request(url, method="POST")
+ with urllib.request.urlopen(request, timeout=60) as f:
+ return json.load(f)
class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
- async def get_unconfigured_account(self) -> Account:
- return await self.deltachat.add_account()
+ def get_unconfigured_account(self) -> Account:
+ return self.deltachat.add_account()
- async def get_unconfigured_bot(self) -> Bot:
- return Bot(await self.get_unconfigured_account())
+ def get_unconfigured_bot(self) -> Bot:
+ return Bot(self.get_unconfigured_account())
- async def new_preconfigured_account(self) -> Account:
+ def new_preconfigured_account(self) -> Account:
"""Make a new account with configuration options set, but configuration not started."""
- credentials = await get_temp_credentials()
- account = await self.get_unconfigured_account()
- await account.set_config("addr", credentials["email"])
- await account.set_config("mail_pw", credentials["password"])
- assert not await account.is_configured()
+ credentials = get_temp_credentials()
+ account = self.get_unconfigured_account()
+ account.set_config("addr", credentials["email"])
+ account.set_config("mail_pw", credentials["password"])
+ assert not account.is_configured()
return account
- async def new_configured_account(self) -> Account:
- account = await self.new_preconfigured_account()
- await account.configure()
- assert await account.is_configured()
+ def new_configured_account(self) -> Account:
+ account = self.new_preconfigured_account()
+ account.configure()
+ assert account.is_configured()
return account
- async def new_configured_bot(self) -> Bot:
- credentials = await get_temp_credentials()
- bot = await self.get_unconfigured_bot()
- await bot.configure(credentials["email"], credentials["password"])
+ def new_configured_bot(self) -> Bot:
+ credentials = get_temp_credentials()
+ bot = self.get_unconfigured_bot()
+ bot.configure(credentials["email"], credentials["password"])
return bot
- async def get_online_account(self) -> Account:
- account = await self.new_configured_account()
- await account.start_io()
+ def get_online_account(self) -> Account:
+ account = self.new_configured_account()
+ account.start_io()
while True:
- event = await account.wait_for_event()
- print(event)
- if event.type == EventType.IMAP_INBOX_IDLE:
+ event = account.wait_for_event()
+ if event.kind == EventType.IMAP_INBOX_IDLE:
break
return account
- async def get_online_accounts(self, num: int) -> List[Account]:
- return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
+ def get_online_accounts(self, num: int) -> List[Account]:
+ return [self.get_online_account() for _ in range(num)]
- async def send_message(
+ def send_message(
self,
to_account: Account,
from_account: Optional[Account] = None,
@@ -74,16 +70,16 @@ class ACFactory:
group: Optional[str] = None,
) -> Message:
if not from_account:
- from_account = (await self.get_online_accounts(1))[0]
- to_contact = await from_account.create_contact(await to_account.get_config("addr"))
+ from_account = (self.get_online_accounts(1))[0]
+ to_contact = from_account.create_contact(to_account.get_config("addr"))
if group:
- to_chat = await from_account.create_group(group)
- await to_chat.add_contact(to_contact)
+ to_chat = from_account.create_group(group)
+ to_chat.add_contact(to_contact)
else:
- to_chat = await to_contact.create_chat()
- return await to_chat.send_message(text=text, file=file)
+ to_chat = to_contact.create_chat()
+ return to_chat.send_message(text=text, file=file)
- async def process_message(
+ def process_message(
self,
to_client: Client,
from_account: Optional[Account] = None,
@@ -91,7 +87,7 @@ class ACFactory:
file: Optional[str] = None,
group: Optional[str] = None,
) -> AttrDict:
- await self.send_message(
+ self.send_message(
to_account=to_client.account,
from_account=from_account,
text=text,
@@ -99,16 +95,16 @@ class ACFactory:
group=group,
)
- return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
+ return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
-@pytest_asyncio.fixture
-async def rpc(tmp_path) -> AsyncGenerator:
+@pytest.fixture()
+def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
- async with rpc_server:
+ with rpc_server:
yield rpc_server
-@pytest_asyncio.fixture
-async def acfactory(rpc) -> AsyncGenerator:
- yield ACFactory(DeltaChat(rpc))
+@pytest.fixture()
+def acfactory(rpc) -> AsyncGenerator:
+ return ACFactory(DeltaChat(rpc))
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py
index f15c1a29a..4a181c528 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py
@@ -1,6 +1,10 @@
-import asyncio
import json
+import logging
import os
+import subprocess
+import sys
+from queue import Queue
+from threading import Event, Thread
from typing import Any, Dict, Optional
@@ -10,7 +14,7 @@ class JsonRpcError(Exception):
class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
- """The given arguments will be passed to asyncio.create_subprocess_exec()"""
+ """The given arguments will be passed to subprocess.Popen()"""
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
@@ -18,81 +22,142 @@ class Rpc:
}
self._kwargs = kwargs
- self.process: asyncio.subprocess.Process
+ self.process: subprocess.Popen
self.id: int
- self.event_queues: Dict[int, asyncio.Queue]
- # Map from request ID to `asyncio.Future` returning the response.
- self.request_events: Dict[int, asyncio.Future]
- self.reader_task: asyncio.Task
+ self.event_queues: Dict[int, Queue]
+ # Map from request ID to `threading.Event`.
+ self.request_events: Dict[int, Event]
+ # Map from request ID to the result.
+ self.request_results: Dict[int, Any]
+ self.request_queue: Queue[Any]
+ self.closing: bool
+ self.reader_thread: Thread
+ self.writer_thread: Thread
+ self.events_thread: Thread
- async def start(self) -> None:
- self.process = await asyncio.create_subprocess_exec(
- "deltachat-rpc-server",
- stdin=asyncio.subprocess.PIPE,
- stdout=asyncio.subprocess.PIPE,
- **self._kwargs,
- )
+ def start(self) -> None:
+ if sys.version_info >= (3, 11):
+ self.process = subprocess.Popen(
+ "deltachat-rpc-server",
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ # Prevent subprocess from capturing SIGINT.
+ process_group=0,
+ **self._kwargs,
+ )
+ else:
+ self.process = subprocess.Popen(
+ "deltachat-rpc-server",
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ # `process_group` is not supported before Python 3.11.
+ preexec_fn=os.setpgrp, # noqa: PLW1509
+ **self._kwargs,
+ )
self.id = 0
self.event_queues = {}
self.request_events = {}
- self.reader_task = asyncio.create_task(self.reader_loop())
+ self.request_results = {}
+ self.request_queue = Queue()
+ self.closing = False
+ self.reader_thread = Thread(target=self.reader_loop)
+ self.reader_thread.start()
+ self.writer_thread = Thread(target=self.writer_loop)
+ self.writer_thread.start()
+ self.events_thread = Thread(target=self.events_loop)
+ self.events_thread.start()
- async def close(self) -> None:
+ def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
- self.process.terminate()
- await self.reader_task
+ self.closing = True
+ self.stop_io_for_all_accounts()
+ self.events_thread.join()
+ self.process.stdin.close()
+ self.reader_thread.join()
+ self.request_queue.put(None)
+ self.writer_thread.join()
- async def __aenter__(self):
- await self.start()
+ def __enter__(self):
+ self.start()
return self
- async def __aexit__(self, _exc_type, _exc, _tb):
- await self.close()
+ def __exit__(self, _exc_type, _exc, _tb):
+ self.close()
- async def reader_loop(self) -> None:
- while True:
- line = await self.process.stdout.readline() # noqa
- if not line: # EOF
- break
- response = json.loads(line)
- if "id" in response:
- fut = self.request_events.pop(response["id"])
- fut.set_result(response)
- elif response["method"] == "event":
- # An event notification.
- params = response["params"]
- account_id = params["contextId"]
- if account_id not in self.event_queues:
- self.event_queues[account_id] = asyncio.Queue()
- await self.event_queues[account_id].put(params["event"])
- else:
- print(response)
+ def reader_loop(self) -> None:
+ try:
+ while True:
+ line = self.process.stdout.readline()
+ if not line: # EOF
+ break
+ response = json.loads(line)
+ if "id" in response:
+ response_id = response["id"]
+ event = self.request_events.pop(response_id)
+ self.request_results[response_id] = response
+ event.set()
+ else:
+ logging.warning("Got a response without ID: %s", response)
+ except Exception:
+ # Log an exception if the reader loop dies.
+ logging.exception("Exception in the reader loop")
- async def wait_for_event(self, account_id: int) -> Optional[dict]:
+ def writer_loop(self) -> None:
+ """Writer loop ensuring only a single thread writes requests."""
+ try:
+ while True:
+ request = self.request_queue.get()
+ if not request:
+ break
+ data = (json.dumps(request) + "\n").encode()
+ self.process.stdin.write(data)
+ self.process.stdin.flush()
+
+ except Exception:
+ # Log an exception if the writer loop dies.
+ logging.exception("Exception in the writer loop")
+
+ def get_queue(self, account_id: int) -> Queue:
+ if account_id not in self.event_queues:
+ self.event_queues[account_id] = Queue()
+ return self.event_queues[account_id]
+
+ def events_loop(self) -> None:
+ """Requests new events and distributes them between queues."""
+ try:
+ while True:
+ if self.closing:
+ return
+ event = self.get_next_event()
+ account_id = event["contextId"]
+ queue = self.get_queue(account_id)
+ queue.put(event["event"])
+ except Exception:
+ # Log an exception if the event loop dies.
+ logging.exception("Exception in the event loop")
+
+ def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
- if account_id in self.event_queues:
- return await self.event_queues[account_id].get()
- return None
+ queue = self.get_queue(account_id)
+ return queue.get()
def __getattr__(self, attr: str):
- async def method(*args, **kwargs) -> Any:
+ def method(*args) -> Any:
self.id += 1
request_id = self.id
- assert not (args and kwargs), "Mixing positional and keyword arguments"
-
request = {
"jsonrpc": "2.0",
"method": attr,
- "params": kwargs or args,
+ "params": args,
"id": self.id,
}
- data = (json.dumps(request) + "\n").encode()
- self.process.stdin.write(data) # noqa
- loop = asyncio.get_running_loop()
- fut = loop.create_future()
- self.request_events[request_id] = fut
- response = await fut
+ event = Event()
+ self.request_events[request_id] = event
+ self.request_queue.put(request)
+ event.wait()
+
+ response = self.request_results.pop(request_id)
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py
index d17c28a16..f91f9844e 100644
--- a/deltachat-rpc-client/tests/test_something.py
+++ b/deltachat-rpc-client/tests/test_something.py
@@ -1,4 +1,6 @@
-import asyncio
+import concurrent.futures
+import json
+import subprocess
from unittest.mock import MagicMock
import pytest
@@ -6,26 +8,26 @@ from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
-@pytest.mark.asyncio()
-async def test_system_info(rpc) -> None:
- system_info = await rpc.get_system_info()
+def test_system_info(rpc) -> None:
+ system_info = rpc.get_system_info()
assert "arch" in system_info
assert "deltachat_core_version" in system_info
-@pytest.mark.asyncio()
-async def test_sleep(rpc) -> None:
+def test_sleep(rpc) -> None:
"""Test that long-running task does not block short-running task from completion."""
- sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
- sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
- done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
- assert sleep_3_task in done
- assert sleep_5_task in pending
- sleep_5_task.cancel()
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
+ sleep_5_future = executor.submit(rpc.sleep, 5.0)
+ sleep_3_future = executor.submit(rpc.sleep, 3.0)
+ done, pending = concurrent.futures.wait(
+ [sleep_5_future, sleep_3_future],
+ return_when=concurrent.futures.FIRST_COMPLETED,
+ )
+ assert sleep_3_future in done
+ assert sleep_5_future in pending
-@pytest.mark.asyncio()
-async def test_email_address_validity(rpc) -> None:
+def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
@@ -33,17 +35,16 @@ async def test_email_address_validity(rpc) -> None:
invalid_addresses = ["email@", "example.com", "emai221"]
for addr in valid_addresses:
- assert await rpc.check_email_validity(addr)
+ assert rpc.check_email_validity(addr)
for addr in invalid_addresses:
- assert not await rpc.check_email_validity(addr)
+ assert not rpc.check_email_validity(addr)
-@pytest.mark.asyncio()
-async def test_acfactory(acfactory) -> None:
- account = await acfactory.new_configured_account()
+def test_acfactory(acfactory) -> None:
+ account = acfactory.new_configured_account()
while True:
- event = await account.wait_for_event()
- if event.type == EventType.CONFIGURE_PROGRESS:
+ event = account.wait_for_event()
+ if event.kind == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success
break
@@ -52,234 +53,241 @@ async def test_acfactory(acfactory) -> None:
print("Successful configuration")
-@pytest.mark.asyncio()
-async def test_configure_starttls(acfactory) -> None:
- account = await acfactory.new_preconfigured_account()
+def test_configure_starttls(acfactory) -> None:
+ account = acfactory.new_preconfigured_account()
# Use STARTTLS
- await account.set_config("mail_security", "2")
- await account.set_config("send_security", "2")
- await account.configure()
- assert await account.is_configured()
+ account.set_config("mail_security", "2")
+ account.set_config("send_security", "2")
+ account.configure()
+ assert account.is_configured()
-@pytest.mark.asyncio()
-async def test_account(acfactory) -> None:
- alice, bob = await acfactory.get_online_accounts(2)
+def test_account(acfactory) -> None:
+ alice, bob = acfactory.get_online_accounts(2)
- bob_addr = await bob.get_config("addr")
- alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
- alice_chat_bob = await alice_contact_bob.create_chat()
- await alice_chat_bob.send_text("Hello!")
+ bob_addr = bob.get_config("addr")
+ alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_chat_bob = alice_contact_bob.create_chat()
+ alice_chat_bob.send_text("Hello!")
while True:
- event = await bob.wait_for_event()
- if event.type == EventType.INCOMING_MSG:
+ event = bob.wait_for_event()
+ if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
- snapshot = await message.get_snapshot()
+ snapshot = message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
- await bob.mark_seen_messages([message])
+ bob.mark_seen_messages([message])
assert alice != bob
assert repr(alice)
- assert (await alice.get_info()).level
- assert await alice.get_size()
- assert await alice.is_configured()
- assert not await alice.get_avatar()
- assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
- assert await alice.get_contacts()
- assert await alice.get_contacts(snapshot=True)
+ assert alice.get_info().level
+ assert alice.get_size()
+ assert alice.is_configured()
+ assert not alice.get_avatar()
+ assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
+ assert alice.get_contacts()
+ assert alice.get_contacts(snapshot=True)
assert alice.self_contact
- assert await alice.get_chatlist()
- assert await alice.get_chatlist(snapshot=True)
- assert await alice.get_qr_code()
- assert await alice.get_fresh_messages()
- assert await alice.get_next_messages()
+ assert alice.get_chatlist()
+ assert alice.get_chatlist(snapshot=True)
+ assert alice.get_qr_code()
+ assert alice.get_fresh_messages()
+ assert alice.get_next_messages()
- group = await alice.create_group("test group")
- await group.add_contact(alice_contact_bob)
- group_msg = await group.send_message(text="hello")
+ # Test sending empty message.
+ assert len(bob.wait_next_messages()) == 0
+ alice_chat_bob.send_text("")
+ messages = bob.wait_next_messages()
+ assert len(messages) == 1
+ message = messages[0]
+ snapshot = message.get_snapshot()
+ assert snapshot.text == ""
+ bob.mark_seen_messages([message])
+
+ group = alice.create_group("test group")
+ group.add_contact(alice_contact_bob)
+ group_msg = group.send_message(text="hello")
assert group_msg == alice.get_message_by_id(group_msg.id)
assert group == alice.get_chat_by_id(group.id)
- await alice.delete_messages([group_msg])
+ alice.delete_messages([group_msg])
- await alice.set_config("selfstatus", "test")
- assert await alice.get_config("selfstatus") == "test"
- await alice.update_config(selfstatus="test2")
- assert await alice.get_config("selfstatus") == "test2"
+ alice.set_config("selfstatus", "test")
+ assert alice.get_config("selfstatus") == "test"
+ alice.update_config(selfstatus="test2")
+ assert alice.get_config("selfstatus") == "test2"
- assert not await alice.get_blocked_contacts()
- await alice_contact_bob.block()
- blocked_contacts = await alice.get_blocked_contacts()
+ assert not alice.get_blocked_contacts()
+ alice_contact_bob.block()
+ blocked_contacts = alice.get_blocked_contacts()
assert blocked_contacts
assert blocked_contacts[0].contact == alice_contact_bob
- await bob.remove()
- await alice.stop_io()
+ bob.remove()
+ alice.stop_io()
-@pytest.mark.asyncio()
-async def test_chat(acfactory) -> None:
- alice, bob = await acfactory.get_online_accounts(2)
+def test_chat(acfactory) -> None:
+ alice, bob = acfactory.get_online_accounts(2)
- bob_addr = await bob.get_config("addr")
- alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
- alice_chat_bob = await alice_contact_bob.create_chat()
- await alice_chat_bob.send_text("Hello!")
+ bob_addr = bob.get_config("addr")
+ alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_chat_bob = alice_contact_bob.create_chat()
+ alice_chat_bob.send_text("Hello!")
while True:
- event = await bob.wait_for_event()
- if event.type == EventType.INCOMING_MSG:
+ event = bob.wait_for_event()
+ if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
- snapshot = await message.get_snapshot()
+ snapshot = message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
bob_chat_alice = bob.get_chat_by_id(chat_id)
assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob)
- await alice_chat_bob.delete()
- assert not await bob_chat_alice.can_send()
- await bob_chat_alice.accept()
- assert await bob_chat_alice.can_send()
- await bob_chat_alice.block()
- bob_chat_alice = await snapshot.sender.create_chat()
- await bob_chat_alice.mute()
- await bob_chat_alice.unmute()
- await bob_chat_alice.pin()
- await bob_chat_alice.unpin()
- await bob_chat_alice.archive()
- await bob_chat_alice.unarchive()
+ alice_chat_bob.delete()
+ assert not bob_chat_alice.can_send()
+ bob_chat_alice.accept()
+ assert bob_chat_alice.can_send()
+ bob_chat_alice.block()
+ bob_chat_alice = snapshot.sender.create_chat()
+ bob_chat_alice.mute()
+ bob_chat_alice.unmute()
+ bob_chat_alice.pin()
+ bob_chat_alice.unpin()
+ bob_chat_alice.archive()
+ bob_chat_alice.unarchive()
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
- await bob_chat_alice.set_name("test")
- await bob_chat_alice.set_ephemeral_timer(300)
- await bob_chat_alice.get_encryption_info()
+ bob_chat_alice.set_name("test")
+ bob_chat_alice.set_ephemeral_timer(300)
+ bob_chat_alice.get_encryption_info()
- group = await alice.create_group("test group")
- await group.add_contact(alice_contact_bob)
- await group.get_qr_code()
+ group = alice.create_group("test group")
+ group.add_contact(alice_contact_bob)
+ group.get_qr_code()
- snapshot = await group.get_basic_snapshot()
+ snapshot = group.get_basic_snapshot()
assert snapshot.name == "test group"
- await group.set_name("new name")
- snapshot = await group.get_full_snapshot()
+ group.set_name("new name")
+ snapshot = group.get_full_snapshot()
assert snapshot.name == "new name"
- msg = await group.send_message(text="hi")
- assert (await msg.get_snapshot()).text == "hi"
- await group.forward_messages([msg])
+ msg = group.send_message(text="hi")
+ assert (msg.get_snapshot()).text == "hi"
+ group.forward_messages([msg])
- await group.set_draft(text="test draft")
- draft = await group.get_draft()
+ group.set_draft(text="test draft")
+ draft = group.get_draft()
assert draft.text == "test draft"
- await group.remove_draft()
- assert not await group.get_draft()
+ group.remove_draft()
+ assert not group.get_draft()
- assert await group.get_messages()
- await group.get_fresh_message_count()
- await group.mark_noticed()
- assert await group.get_contacts()
- await group.remove_contact(alice_chat_bob)
- await group.get_locations()
+ assert group.get_messages()
+ group.get_fresh_message_count()
+ group.mark_noticed()
+ assert group.get_contacts()
+ group.remove_contact(alice_chat_bob)
+ group.get_locations()
-@pytest.mark.asyncio()
-async def test_contact(acfactory) -> None:
- alice, bob = await acfactory.get_online_accounts(2)
+def test_contact(acfactory) -> None:
+ alice, bob = acfactory.get_online_accounts(2)
- bob_addr = await bob.get_config("addr")
- alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
+ bob_addr = bob.get_config("addr")
+ alice_contact_bob = alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob)
- await alice_contact_bob.block()
- await alice_contact_bob.unblock()
- await alice_contact_bob.set_name("new name")
- await alice_contact_bob.get_encryption_info()
- snapshot = await alice_contact_bob.get_snapshot()
+ alice_contact_bob.block()
+ alice_contact_bob.unblock()
+ alice_contact_bob.set_name("new name")
+ alice_contact_bob.get_encryption_info()
+ snapshot = alice_contact_bob.get_snapshot()
assert snapshot.address == bob_addr
- await alice_contact_bob.create_chat()
+ alice_contact_bob.create_chat()
-@pytest.mark.asyncio()
-async def test_message(acfactory) -> None:
- alice, bob = await acfactory.get_online_accounts(2)
+def test_message(acfactory) -> None:
+ alice, bob = acfactory.get_online_accounts(2)
- bob_addr = await bob.get_config("addr")
- alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
- alice_chat_bob = await alice_contact_bob.create_chat()
- await alice_chat_bob.send_text("Hello!")
+ bob_addr = bob.get_config("addr")
+ alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_chat_bob = alice_contact_bob.create_chat()
+ alice_chat_bob.send_text("Hello!")
while True:
- event = await bob.wait_for_event()
- if event.type == EventType.INCOMING_MSG:
+ event = bob.wait_for_event()
+ if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
- snapshot = await message.get_snapshot()
+ snapshot = message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
assert not snapshot.is_bot
assert repr(message)
with pytest.raises(JsonRpcError): # chat is not accepted
- await snapshot.chat.send_text("hi")
- await snapshot.chat.accept()
- await snapshot.chat.send_text("hi")
+ snapshot.chat.send_text("hi")
+ snapshot.chat.accept()
+ snapshot.chat.send_text("hi")
- await message.mark_seen()
- await message.send_reaction("😎")
+ message.mark_seen()
+ message.send_reaction("😎")
+ reactions = message.get_reactions()
+ assert reactions
+ snapshot = message.get_snapshot()
+ assert reactions == snapshot.reactions
-@pytest.mark.asyncio()
-async def test_is_bot(acfactory) -> None:
+def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
- alice, bob = await acfactory.get_online_accounts(2)
+ alice, bob = acfactory.get_online_accounts(2)
- bob_addr = await bob.get_config("addr")
- alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
- alice_chat_bob = await alice_contact_bob.create_chat()
+ bob_addr = bob.get_config("addr")
+ alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_chat_bob = alice_contact_bob.create_chat()
# Alice becomes a bot.
- await alice.set_config("bot", "1")
- await alice_chat_bob.send_text("Hello!")
+ alice.set_config("bot", "1")
+ alice_chat_bob.send_text("Hello!")
while True:
- event = await bob.wait_for_event()
- if event.type == EventType.INCOMING_MSG:
+ event = bob.wait_for_event()
+ if event.kind == EventType.INCOMING_MSG:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
- snapshot = await message.get_snapshot()
+ snapshot = message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
break
-@pytest.mark.asyncio()
-async def test_bot(acfactory) -> None:
+def test_bot(acfactory) -> None:
mock = MagicMock()
- user = (await acfactory.get_online_accounts(1))[0]
- bot = await acfactory.new_configured_bot()
- bot2 = await acfactory.new_configured_bot()
+ user = (acfactory.get_online_accounts(1))[0]
+ bot = acfactory.new_configured_bot()
+ bot2 = acfactory.new_configured_bot()
- assert await bot.is_configured()
- assert await bot.account.get_config("bot") == "1"
+ assert bot.is_configured()
+ assert bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook)
- event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
- snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
+ event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
+ snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook)
@@ -291,43 +299,62 @@ async def test_bot(acfactory) -> None:
hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help"))
- event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
+ event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id)
- event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
+ event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
- await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
+ acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
- await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
+ acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook)
mock.hook.reset_mock()
- await acfactory.process_message(from_account=user, to_client=bot, text="hello")
- event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
+ acfactory.process_message(from_account=user, to_client=bot, text="hello")
+ event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id)
-@pytest.mark.asyncio()
-async def test_wait_next_messages(acfactory) -> None:
- alice = await acfactory.new_configured_account()
+def test_wait_next_messages(acfactory) -> None:
+ alice = acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
- bot = await acfactory.new_preconfigured_account()
- await bot.set_config("bot", "1")
- await bot.configure()
+ bot = acfactory.new_preconfigured_account()
+ bot.set_config("bot", "1")
+ bot.configure()
# There are no old messages and the call returns immediately.
- assert not await bot.wait_next_messages()
+ assert not bot.wait_next_messages()
- # Bot starts waiting for messages.
- next_messages_task = asyncio.create_task(bot.wait_next_messages())
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+ # Bot starts waiting for messages.
+ next_messages_task = executor.submit(bot.wait_next_messages)
- bot_addr = await bot.get_config("addr")
- alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
- alice_chat_bot = await alice_contact_bot.create_chat()
- await alice_chat_bot.send_text("Hello!")
+ bot_addr = bot.get_config("addr")
+ alice_contact_bot = alice.create_contact(bot_addr, "Bob")
+ alice_chat_bot = alice_contact_bot.create_chat()
+ alice_chat_bot.send_text("Hello!")
- next_messages = await next_messages_task
- assert len(next_messages) == 1
- snapshot = await next_messages[0].get_snapshot()
- assert snapshot.text == "Hello!"
+ next_messages = next_messages_task.result()
+ assert len(next_messages) == 1
+ snapshot = next_messages[0].get_snapshot()
+ assert snapshot.text == "Hello!"
+
+
+def test_import_export(acfactory, tmp_path) -> None:
+ alice = acfactory.new_configured_account()
+ alice.export_backup(tmp_path)
+
+ files = list(tmp_path.glob("*.tar"))
+ alice2 = acfactory.get_unconfigured_account()
+ alice2.import_backup(files[0])
+
+ assert alice2.manager.get_system_info()
+
+
+def test_openrpc_command_line() -> None:
+ """Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
+ out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
+ openrpc = json.loads(out)
+ assert "openrpc" in openrpc
+ assert "methods" in openrpc
diff --git a/deltachat-rpc-client/tests/test_webxdc.py b/deltachat-rpc-client/tests/test_webxdc.py
index 8a0584d03..8f3d8edcb 100644
--- a/deltachat-rpc-client/tests/test_webxdc.py
+++ b/deltachat-rpc-client/tests/test_webxdc.py
@@ -1,24 +1,22 @@
-import pytest
from deltachat_rpc_client import EventType
-@pytest.mark.asyncio()
-async def test_webxdc(acfactory) -> None:
- alice, bob = await acfactory.get_online_accounts(2)
+def test_webxdc(acfactory) -> None:
+ alice, bob = acfactory.get_online_accounts(2)
- bob_addr = await bob.get_config("addr")
- alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
- alice_chat_bob = await alice_contact_bob.create_chat()
- await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
+ bob_addr = bob.get_config("addr")
+ alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_chat_bob = alice_contact_bob.create_chat()
+ alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
while True:
- event = await bob.wait_for_event()
- if event.type == EventType.INCOMING_MSG:
+ event = bob.wait_for_event()
+ if event.kind == EventType.INCOMING_MSG:
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id)
break
- webxdc_info = await message.get_webxdc_info()
+ webxdc_info = message.get_webxdc_info()
assert webxdc_info == {
"document": None,
"icon": "icon.png",
@@ -28,20 +26,20 @@ async def test_webxdc(acfactory) -> None:
"summary": None,
}
- status_updates = await message.get_webxdc_status_updates()
+ status_updates = message.get_webxdc_status_updates()
assert status_updates == []
- await bob_chat_alice.accept()
- await message.send_webxdc_status_update({"payload": 42}, "")
- await message.send_webxdc_status_update({"payload": "Second update"}, "description")
+ bob_chat_alice.accept()
+ message.send_webxdc_status_update({"payload": 42}, "")
+ message.send_webxdc_status_update({"payload": "Second update"}, "description")
- status_updates = await message.get_webxdc_status_updates()
+ status_updates = message.get_webxdc_status_updates()
assert status_updates == [
{"payload": 42, "serial": 1, "max_serial": 2},
{"payload": "Second update", "serial": 2, "max_serial": 2},
]
- status_updates = await message.get_webxdc_status_updates(1)
+ status_updates = message.get_webxdc_status_updates(1)
assert status_updates == [
{"payload": "Second update", "serial": 2, "max_serial": 2},
]
diff --git a/deltachat-rpc-client/tox.ini b/deltachat-rpc-client/tox.ini
index 1d0dba56c..4bcdd7e0a 100644
--- a/deltachat-rpc-client/tox.ini
+++ b/deltachat-rpc-client/tox.ini
@@ -6,7 +6,7 @@ envlist =
[testenv]
commands =
- pytest {posargs}
+ pytest -n6 {posargs}
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
@@ -14,10 +14,8 @@ passenv =
DCC_NEW_TMP_EMAIL
deps =
pytest
- pytest-asyncio
pytest-timeout
- aiohttp
- aiodns
+ pytest-xdist
[testenv:lint]
skipsdist = True
diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml
index f66223ade..d088dc614 100644
--- a/deltachat-rpc-server/Cargo.toml
+++ b/deltachat-rpc-server/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
-version = "1.112.6"
+version = "1.126.1"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,13 +15,13 @@ deltachat = { path = "..", default-features = false }
anyhow = "1"
env_logger = { version = "0.10.0" }
-futures-lite = "1.12.0"
+futures-lite = "1.13.0"
log = "0.4"
-serde_json = "1.0.95"
+serde_json = "1.0.105"
serde = { version = "1.0", features = ["derive"] }
-tokio = { version = "1.27.0", features = ["io-std"] }
-tokio-util = "0.7.7"
-yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
+tokio = { version = "1.32.0", features = ["io-std"] }
+tokio-util = "0.7.9"
+yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
[features]
default = ["vendored"]
diff --git a/deltachat-rpc-server/README.md b/deltachat-rpc-server/README.md
index 2027072a5..be8c0c562 100644
--- a/deltachat-rpc-server/README.md
+++ b/deltachat-rpc-server/README.md
@@ -32,3 +32,6 @@ languages other than Rust, for example:
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
+
+Run `deltachat-rpc-server --version` to check the version of the server.
+Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.
diff --git a/deltachat-rpc-server/src/main.rs b/deltachat-rpc-server/src/main.rs
index 87df5fdf4..4be58760b 100644
--- a/deltachat-rpc-server/src/main.rs
+++ b/deltachat-rpc-server/src/main.rs
@@ -1,23 +1,36 @@
+//! Delta Chat core RPC server.
+//!
+//! It speaks JSON Lines over stdio.
use std::env;
-///! Delta Chat core RPC server.
-///!
-///! It speaks JSON Lines over stdio.
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Context as _, Result};
use deltachat::constants::DC_VERSION_STR;
-use deltachat_jsonrpc::api::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
+use yerpc::RpcServer as _;
+
+#[cfg(target_family = "unix")]
+use tokio::signal::unix as signal_unix;
+
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use yerpc::{RpcClient, RpcSession};
#[tokio::main(flavor = "multi_thread")]
-async fn main() -> Result<()> {
+async fn main() {
+ let r = main_impl().await;
+ // From tokio documentation:
+ // "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
+ // thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
+ // until the user presses enter."
+ std::process::exit(if r.is_ok() { 0 } else { 1 });
+}
+
+async fn main_impl() -> Result<()> {
let mut args = env::args_os();
let _program_name = args.next().context("no command line arguments found")?;
if let Some(first_arg) = args.next() {
@@ -27,6 +40,12 @@ async fn main() -> Result<()> {
}
eprintln!("{}", &*DC_VERSION_STR);
return Ok(());
+ } else if first_arg.to_str() == Some("--openrpc") {
+ if let Some(arg) = args.next() {
+ return Err(anyhow!("Unrecognized argument {:?}", arg));
+ }
+ println!("{}", CommandApi::openrpc_specification()?);
+ return Ok(());
} else {
return Err(anyhow!("Unrecognized option {:?}", first_arg));
}
@@ -35,12 +54,17 @@ async fn main() -> Result<()> {
return Err(anyhow!("Unrecognized argument {:?}", arg));
}
+ // Install signal handlers early so that the shutdown is graceful starting from here.
+ let _ctrl_c = tokio::signal::ctrl_c();
+ #[cfg(target_family = "unix")]
+ let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
+
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
- let accounts = Accounts::new(PathBuf::from(&path)).await?;
- let events = accounts.get_event_emitter();
+ let writable = true;
+ let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
log::info!("Creating JSON-RPC API.");
let accounts = Arc::new(RwLock::new(accounts));
@@ -48,28 +72,15 @@ async fn main() -> Result<()> {
let (client, mut out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), state.clone());
- let canceler = CancellationToken::new();
-
- // Events task converts core events to JSON-RPC notifications.
- let events_task: JoinHandle> = tokio::spawn(async move {
- let mut r = Ok(());
- while let Some(event) = events.recv().await {
- if r.is_err() {
- continue;
- }
- let event = event_to_json_rpc_notification(event);
- r = client.send_notification("event", Some(event)).await;
- }
- r?;
- Ok(())
- });
+ let main_cancel = CancellationToken::new();
// Send task prints JSON responses to stdout.
- let cancelable = canceler.clone();
+ let cancel = main_cancel.clone();
let send_task: JoinHandle> = tokio::spawn(async move {
+ let _cancel_guard = cancel.clone().drop_guard();
loop {
let message = tokio::select! {
- _ = cancelable.cancelled() => break,
+ _ = cancel.cancelled() => break,
message = out_receiver.next() => match message {
None => break,
Some(message) => serde_json::to_string(&message)?,
@@ -81,12 +92,32 @@ async fn main() -> Result<()> {
Ok(())
});
+ let cancel = main_cancel.clone();
+ let sigterm_task: JoinHandle> = tokio::spawn(async move {
+ #[cfg(target_family = "unix")]
+ {
+ let _cancel_guard = cancel.clone().drop_guard();
+ tokio::select! {
+ _ = cancel.cancelled() => (),
+ _ = sigterm.recv() => {
+ log::info!("got SIGTERM");
+ }
+ }
+ }
+ let _ = cancel;
+ Ok(())
+ });
+
// Receiver task reads JSON requests from stdin.
+ let cancel = main_cancel.clone();
let recv_task: JoinHandle> = tokio::spawn(async move {
+ let _cancel_guard = cancel.clone().drop_guard();
let stdin = io::stdin();
let mut lines = BufReader::new(stdin).lines();
+
loop {
let message = tokio::select! {
+ _ = cancel.cancelled() => break,
_ = tokio::signal::ctrl_c() => {
log::info!("got ctrl-c event");
break;
@@ -108,18 +139,13 @@ async fn main() -> Result<()> {
Ok(())
});
- // Wait for the end of stdin / ctrl-c.
- recv_task.await?.ok();
-
- // See "Thread safety" section in deltachat-ffi/deltachat.h for explanation.
- // NB: Events are drained by events_task.
- canceler.cancel();
+ main_cancel.cancelled().await;
accounts.read().await.stop_io().await;
+ drop(accounts);
drop(state);
- let (r0, r1) = tokio::join!(events_task, send_task);
- for r in [r0, r1] {
- r??;
- }
+ send_task.await??;
+ sigterm_task.await??;
+ recv_task.await??;
Ok(())
}
diff --git a/deny.toml b/deny.toml
index e2f70f17d..22d06bab4 100644
--- a/deny.toml
+++ b/deny.toml
@@ -2,9 +2,7 @@
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
-
- # Only affects windows if using non-default allocator (and unmaintained).
- "RUSTSEC-2021-0145",
+ "RUSTSEC-2022-0093",
]
[bans]
@@ -17,8 +15,6 @@ skip = [
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
- { name = "clap_lex", version = "0.2.4" },
- { name = "clap", version = "3.2.23" },
{ name = "convert_case", version = "0.4.0" },
{ name = "curve25519-dalek", version = "3.2.0" },
{ name = "darling_core", version = "<0.14" },
@@ -28,12 +24,10 @@ skip = [
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
- { name = "env_logger", version = "<0.10" },
+ { name = "fastrand", version = "1.9.0" },
{ name = "getrandom", version = "<0.2" },
- { name = "hermit-abi", version = "<0.3" },
- { name = "humantime", version = "<2.1" },
- { name = "idna", version = "<0.3" },
- { name = "libm", version = "0.1.4" },
+ { name = "hashbrown", version = "<0.14.0" },
+ { name = "indexmap", version = "<2.0.0" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
@@ -41,20 +35,27 @@ skip = [
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
+ { name = "regex-automata", version = "0.1.10" },
+ { name = "regex-syntax", version = "0.6.29" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
+ { name = "socket2", version = "0.4.9" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "wasi", version = "<0.11" },
- { name = "windows_aarch64_msvc", version = "<0.42" },
- { name = "windows_i686_gnu", version = "<0.42" },
- { name = "windows_i686_msvc", version = "<0.42" },
- { name = "windows-sys", version = "<0.45" },
- { name = "windows_x86_64_gnu", version = "<0.42" },
- { name = "windows_x86_64_msvc", version = "<0.42" },
+ { name = "windows_aarch64_gnullvm", version = "<0.48" },
+ { name = "windows_aarch64_msvc", version = "<0.48" },
+ { name = "windows_i686_gnu", version = "<0.48" },
+ { name = "windows_i686_msvc", version = "<0.48" },
+ { name = "windows-sys", version = "<0.48" },
+ { name = "windows-targets", version = "<0.48" },
+ { name = "windows", version = "0.32.0" },
+ { name = "windows_x86_64_gnullvm", version = "<0.48" },
+ { name = "windows_x86_64_gnu", version = "<0.48" },
+ { name = "windows_x86_64_msvc", version = "<0.48" },
]
diff --git a/draft/aeap-mvp.md b/draft/aeap-mvp.md
index 94ac3c03e..040125f57 100644
--- a/draft/aeap-mvp.md
+++ b/draft/aeap-mvp.md
@@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre
#### Upsides:
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
-- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
+- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
diff --git a/examples/simple.rs b/examples/simple.rs
deleted file mode 100644
index cca50bb27..000000000
--- a/examples/simple.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use deltachat::chat::{self, ChatId};
-use deltachat::chatlist::*;
-use deltachat::config;
-use deltachat::contact::*;
-use deltachat::context::*;
-use deltachat::message::Message;
-use deltachat::stock_str::StockStrings;
-use deltachat::{EventType, Events};
-use tempfile::tempdir;
-
-fn cb(event: EventType) {
- match event {
- EventType::ConfigureProgress { progress, .. } => {
- log::info!("progress: {}", progress);
- }
- EventType::Info(msg) => {
- log::info!("{}", msg);
- }
- EventType::Warning(msg) => {
- log::warn!("{}", msg);
- }
- EventType::Error(msg) => {
- log::error!("{}", msg);
- }
- event => {
- log::info!("{:?}", event);
- }
- }
-}
-
-/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
-#[tokio::main]
-async fn main() {
- pretty_env_logger::try_init_timed().ok();
-
- let dir = tempdir().unwrap();
- let dbfile = dir.path().join("db.sqlite");
- log::info!("creating database {:?}", dbfile);
- let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
- .await
- .expect("Failed to create context");
- let info = ctx.get_info().await;
- log::info!("info: {:#?}", info);
-
- let events = ctx.get_event_emitter();
- let events_spawn = tokio::task::spawn(async move {
- while let Some(event) = events.recv().await {
- cb(event.typ);
- }
- });
-
- log::info!("configuring");
- let args = std::env::args().collect::>();
- assert_eq!(args.len(), 3, "requires email password");
- let email = args[1].clone();
- let pw = args[2].clone();
- ctx.set_config(config::Config::Addr, Some(&email))
- .await
- .unwrap();
- ctx.set_config(config::Config::MailPw, Some(&pw))
- .await
- .unwrap();
-
- ctx.configure().await.unwrap();
-
- log::info!("------ RUN ------");
- ctx.start_io().await;
- log::info!("--- SENDING A MESSAGE ---");
-
- let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
- .await
- .unwrap();
- let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
-
- for i in 0..1 {
- log::info!("sending message {}", i);
- chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
- .await
- .unwrap();
- }
-
- // wait for the message to be sent out
- tokio::time::sleep(std::time::Duration::from_secs(1)).await;
-
- log::info!("fetching chats..");
- let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
-
- for i in 0..chats.len() {
- let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
- .await
- .unwrap();
- log::info!("[{}] msg: {:?}", i, msg);
- }
-
- log::info!("stopping");
- ctx.stop_io().await;
- log::info!("closing");
- drop(ctx);
- events_spawn.await.unwrap();
-}
diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock
index e9033c1cf..feb204237 100644
--- a/fuzz/Cargo.lock
+++ b/fuzz/Cargo.lock
@@ -2,12 +2,6 @@
# It is not intended for manual editing.
version = 3
-[[package]]
-name = "Inflector"
-version = "0.11.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
-
[[package]]
name = "abao"
version = "0.2.0"
@@ -60,19 +54,13 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "0.7.20"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
dependencies = [
"memchr",
]
-[[package]]
-name = "aliasable"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
-
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
@@ -189,39 +177,26 @@ dependencies = [
[[package]]
name = "async-imap"
-version = "0.7.0"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8379e2f1cdeb79afd2006932d7e8f64993fc0f7386d0ebc37231c90b05968c25"
+checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
dependencies = [
"async-channel",
- "async-native-tls 0.4.0",
"base64 0.21.0",
- "byte-pool",
+ "bytes",
"chrono",
"futures",
"imap-proto",
"log",
"nom",
"once_cell",
- "ouroboros",
"pin-utils",
+ "self_cell",
"stop-token",
"thiserror",
"tokio",
]
-[[package]]
-name = "async-native-tls"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe"
-dependencies = [
- "native-tls",
- "thiserror",
- "tokio",
- "url",
-]
-
[[package]]
name = "async-native-tls"
version = "0.5.0"
@@ -362,9 +337,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.0.2"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "blake3"
@@ -554,16 +529,6 @@ version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
-[[package]]
-name = "byte-pool"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca"
-dependencies = [
- "crossbeam-queue",
- "stable_deref_trait",
-]
-
[[package]]
name = "bytemuck"
version = "1.12.3"
@@ -766,16 +731,6 @@ dependencies = [
"cfg-if",
]
-[[package]]
-name = "crossbeam-queue"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
-dependencies = [
- "cfg-if",
- "crossbeam-utils",
-]
-
[[package]]
name = "crossbeam-utils"
version = "0.8.14"
@@ -951,12 +906,12 @@ dependencies = [
[[package]]
name = "deltachat"
-version = "1.112.6"
+version = "1.123.0"
dependencies = [
"anyhow",
"async-channel",
"async-imap",
- "async-native-tls 0.5.0",
+ "async-native-tls",
"async-smtp",
"async_zip",
"backtrace",
@@ -968,6 +923,7 @@ dependencies = [
"encoded-words",
"escaper",
"fast-socks5",
+ "fd-lock",
"format-flowed",
"futures",
"futures-lite",
@@ -979,7 +935,8 @@ dependencies = [
"lettre_email",
"libc",
"mailparse 0.14.0",
- "num-derive",
+ "mime",
+ "num-derive 0.4.0",
"num-traits",
"num_cpus",
"once_cell",
@@ -1244,7 +1201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
dependencies = [
"serde",
- "signature 1.6.4",
+ "signature 2.1.0",
]
[[package]]
@@ -1453,14 +1410,14 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "enum-as-inner"
-version = "0.5.1"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
+checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 1.0.107",
+ "syn 2.0.15",
]
[[package]]
@@ -1488,6 +1445,17 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "errno"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
@@ -1556,6 +1524,17 @@ dependencies = [
"instant",
]
+[[package]]
+name = "fd-lock"
+version = "3.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
+dependencies = [
+ "cfg-if",
+ "rustix 0.38.14",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "ff"
version = "0.12.1"
@@ -1640,9 +1619,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
-version = "1.1.0"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
dependencies = [
"percent-encoding",
]
@@ -1701,9 +1680,9 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
-version = "1.12.0"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand",
"futures-core",
@@ -1867,18 +1846,15 @@ dependencies = [
[[package]]
name = "heck"
-version = "0.4.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
-version = "0.2.6"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
-dependencies = [
- "libc",
-]
+checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "hex"
@@ -1975,7 +1951,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
- "socket2",
+ "socket2 0.4.7",
"tokio",
"tower-service",
"tracing",
@@ -2036,20 +2012,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
-version = "0.2.3"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
-dependencies = [
- "matches",
- "unicode-bidi",
- "unicode-normalization",
-]
-
-[[package]]
-name = "idna"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@@ -2057,9 +2022,9 @@ dependencies = [
[[package]]
name = "image"
-version = "0.24.6"
+version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
+checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
dependencies = [
"bytemuck",
"byteorder",
@@ -2124,10 +2089,10 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be"
dependencies = [
- "socket2",
+ "socket2 0.4.7",
"widestring",
"winapi",
- "winreg",
+ "winreg 0.10.1",
]
[[package]]
@@ -2254,9 +2219,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.139"
+version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
[[package]]
name = "libm"
@@ -2303,6 +2268,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
+
[[package]]
name = "lock_api"
version = "0.4.9"
@@ -2365,15 +2336,9 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
- "regex-automata",
+ "regex-automata 0.1.10",
]
-[[package]]
-name = "matches"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
-
[[package]]
name = "md-5"
version = "0.10.5"
@@ -2391,15 +2356,15 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1"
[[package]]
name = "memchr"
-version = "2.5.0"
+version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "mime"
-version = "0.3.16"
+version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
@@ -2418,14 +2383,13 @@ dependencies = [
[[package]]
name = "mio"
-version = "0.8.5"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
+checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
- "log",
"wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.42.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -2579,6 +2543,17 @@ dependencies = [
"syn 1.0.107",
]
+[[package]]
+name = "num-derive"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
[[package]]
name = "num-integer"
version = "0.1.45"
@@ -2623,9 +2598,9 @@ dependencies = [
[[package]]
name = "num_cpus"
-version = "1.15.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
@@ -2651,9 +2626,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.17.0"
+version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
@@ -2663,9 +2638,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
-version = "0.10.48"
+version = "0.10.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2"
+checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
@@ -2704,11 +2679,10 @@ dependencies = [
[[package]]
name = "openssl-sys"
-version = "0.9.83"
+version = "0.9.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b"
+checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
dependencies = [
- "autocfg",
"cc",
"libc",
"openssl-src",
@@ -2716,29 +2690,6 @@ dependencies = [
"vcpkg",
]
-[[package]]
-name = "ouroboros"
-version = "0.15.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca"
-dependencies = [
- "aliasable",
- "ouroboros_macro",
-]
-
-[[package]]
-name = "ouroboros_macro"
-version = "0.15.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d"
-dependencies = [
- "Inflector",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn 1.0.107",
-]
-
[[package]]
name = "overload"
version = "0.1.1"
@@ -2865,9 +2816,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pgp"
@@ -2902,7 +2853,7 @@ dependencies = [
"md-5",
"nom",
"num-bigint-dig",
- "num-derive",
+ "num-derive 0.3.3",
"num-traits",
"p256 0.13.1",
"p384 0.13.0",
@@ -2942,9 +2893,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
-version = "0.2.9"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@@ -3135,9 +3086,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
-version = "0.28.2"
+version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
+checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
"memchr",
]
@@ -3162,9 +3113,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
-version = "0.9.2"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9"
+checksum = "c956be1b23f4261676aed05a0046e204e8a6836e50203902683a718af0797989"
dependencies = [
"bytes",
"rand 0.8.5",
@@ -3187,7 +3138,7 @@ checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
dependencies = [
"libc",
"quinn-proto",
- "socket2",
+ "socket2 0.4.7",
"tracing",
"windows-sys 0.42.0",
]
@@ -3316,13 +3267,14 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.7.0"
+version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax",
+ "regex-automata 0.3.8",
+ "regex-syntax 0.7.5",
]
[[package]]
@@ -3331,7 +3283,18 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
- "regex-syntax",
+ "regex-syntax 0.6.28",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.7.5",
]
[[package]]
@@ -3341,10 +3304,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
-name = "reqwest"
-version = "0.11.16"
+name = "regex-syntax"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
+checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+
+[[package]]
+name = "reqwest"
+version = "0.11.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [
"base64 0.21.0",
"bytes",
@@ -3374,7 +3343,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
- "winreg",
+ "winreg 0.50.0",
]
[[package]]
@@ -3480,7 +3449,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
- "bitflags 2.0.2",
+ "bitflags 2.4.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@@ -3531,13 +3500,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [
"bitflags 1.3.2",
- "errno",
+ "errno 0.2.8",
"io-lifetimes",
"libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.1.4",
"windows-sys 0.42.0",
]
+[[package]]
+name = "rustix"
+version = "0.38.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
+dependencies = [
+ "bitflags 2.4.0",
+ "errno 0.3.3",
+ "libc",
+ "linux-raw-sys 0.4.7",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "rustls"
version = "0.20.8"
@@ -3599,9 +3581,9 @@ dependencies = [
[[package]]
name = "sanitize-filename"
-version = "0.4.0"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c"
+checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603"
dependencies = [
"lazy_static",
"regex",
@@ -3690,6 +3672,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "self_cell"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
+
[[package]]
name = "semver"
version = "1.0.17"
@@ -3891,6 +3879,16 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "socket2"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "spin"
version = "0.5.2"
@@ -3955,12 +3953,6 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "stable_deref_trait"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
-
[[package]]
name = "stop-token"
version = "0.7.0"
@@ -3981,21 +3973,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
-version = "0.24.1"
+version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
[[package]]
name = "strum_macros"
-version = "0.24.3"
+version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
+checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
- "syn 1.0.107",
+ "syn 2.0.15",
]
[[package]]
@@ -4074,7 +4066,7 @@ dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
- "rustix",
+ "rustix 0.36.7",
"windows-sys 0.42.0",
]
@@ -4183,22 +4175,21 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
-version = "1.25.0"
+version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
+checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [
- "autocfg",
+ "backtrace",
"bytes",
"libc",
- "memchr",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2",
+ "socket2 0.5.4",
"tokio-macros",
- "windows-sys 0.42.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -4213,13 +4204,13 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "1.8.2"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.107",
+ "syn 2.0.15",
]
[[package]]
@@ -4249,9 +4240,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
-version = "0.1.11"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -4275,9 +4266,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.7"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
+checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
dependencies = [
"bytes",
"futures-core",
@@ -4401,9 +4392,9 @@ dependencies = [
[[package]]
name = "trust-dns-proto"
-version = "0.22.0"
+version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
+checksum = "0dc775440033cb114085f6f2437682b194fa7546466024b1037e82a48a052a69"
dependencies = [
"async-trait",
"cfg-if",
@@ -4412,9 +4403,9 @@ dependencies = [
"futures-channel",
"futures-io",
"futures-util",
- "idna 0.2.3",
+ "idna",
"ipnet",
- "lazy_static",
+ "once_cell",
"rand 0.8.5",
"smallvec",
"thiserror",
@@ -4426,16 +4417,17 @@ dependencies = [
[[package]]
name = "trust-dns-resolver"
-version = "0.22.0"
+version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
+checksum = "2dff7aed33ef3e8bf2c9966fccdfed93f93d46f432282ea875cd66faabc6ef2f"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
- "lazy_static",
"lru-cache",
+ "once_cell",
"parking_lot",
+ "rand 0.8.5",
"resolv-conf",
"smallvec",
"thiserror",
@@ -4467,9 +4459,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "unicode-bidi"
-version = "0.3.8"
+version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
@@ -4516,12 +4508,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
-version = "2.3.1"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [
"form_urlencoded",
- "idna 0.3.0",
+ "idna",
"percent-encoding",
]
@@ -4684,9 +4676,9 @@ dependencies = [
[[package]]
name = "webpki"
-version = "0.22.0"
+version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
dependencies = [
"ring",
"untrusted",
@@ -4767,21 +4759,51 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
- "windows_aarch64_gnullvm",
+ "windows_aarch64_gnullvm 0.42.0",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
- "windows_x86_64_gnullvm",
+ "windows_x86_64_gnullvm 0.42.0",
"windows_x86_64_msvc 0.42.0",
]
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.32.0"
@@ -4800,6 +4822,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
[[package]]
name = "windows_i686_gnu"
version = "0.32.0"
@@ -4818,6 +4846,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
[[package]]
name = "windows_i686_msvc"
version = "0.32.0"
@@ -4836,6 +4870,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.32.0"
@@ -4854,12 +4894,24 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.32.0"
@@ -4878,6 +4930,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
[[package]]
name = "winreg"
version = "0.10.1"
@@ -4887,6 +4945,16 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "x25519-dalek"
version = "2.0.0-pre.1"
diff --git a/node/constants.js b/node/constants.js
index 7752a3dd0..b9c990c77 100644
--- a/node/constants.js
+++ b/node/constants.js
@@ -48,6 +48,7 @@ module.exports = {
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
DC_EVENT_MSGS_NOTICED: 2008,
+ DC_EVENT_MSG_DELETED: 2016,
DC_EVENT_MSG_DELIVERED: 2010,
DC_EVENT_MSG_FAILED: 2012,
DC_EVENT_MSG_READ: 2015,
@@ -89,6 +90,7 @@ module.exports = {
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,
+ DC_KEY_GEN_RSA4096: 3,
DC_LP_AUTH_NORMAL: 4,
DC_LP_AUTH_OAUTH2: 2,
DC_MEDIA_QUALITY_BALANCED: 0,
@@ -151,11 +153,14 @@ module.exports = {
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
DC_STR_ARCHIVEDCHATS: 40,
DC_STR_AUDIO: 11,
+ DC_STR_BACKUP_TRANSFER_MSG_BODY: 163,
DC_STR_BACKUP_TRANSFER_QR: 162,
DC_STR_BAD_TIME_MSG_BODY: 85,
DC_STR_BROADCAST_LIST: 115,
DC_STR_CANNOT_LOGIN: 60,
DC_STR_CANTDECRYPT_MSG_BODY: 29,
+ DC_STR_CHAT_PROTECTION_DISABLED: 171,
+ DC_STR_CHAT_PROTECTION_ENABLED: 170,
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
@@ -241,12 +246,6 @@ module.exports = {
DC_STR_OUTGOING_MESSAGES: 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
- DC_STR_PROTECTION_DISABLED: 89,
- DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
- DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
- DC_STR_PROTECTION_ENABLED: 88,
- DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
- DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
diff --git a/node/events.js b/node/events.js
index d27fd1829..5a24586a2 100644
--- a/node/events.js
+++ b/node/events.js
@@ -22,6 +22,7 @@ module.exports = {
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
2015: 'DC_EVENT_MSG_READ',
+ 2016: 'DC_EVENT_MSG_DELETED',
2020: 'DC_EVENT_CHAT_MODIFIED',
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
2030: 'DC_EVENT_CONTACTS_CHANGED',
diff --git a/node/lib/constants.ts b/node/lib/constants.ts
index 2452ce754..6cc08f144 100644
--- a/node/lib/constants.ts
+++ b/node/lib/constants.ts
@@ -48,6 +48,7 @@ export enum C {
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
DC_EVENT_MSGS_NOTICED = 2008,
+ DC_EVENT_MSG_DELETED = 2016,
DC_EVENT_MSG_DELIVERED = 2010,
DC_EVENT_MSG_FAILED = 2012,
DC_EVENT_MSG_READ = 2015,
@@ -89,6 +90,7 @@ export enum C {
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
+ DC_KEY_GEN_RSA4096 = 3,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
@@ -151,11 +153,14 @@ export enum C {
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
+ DC_STR_BACKUP_TRANSFER_MSG_BODY = 163,
DC_STR_BACKUP_TRANSFER_QR = 162,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
+ DC_STR_CHAT_PROTECTION_DISABLED = 171,
+ DC_STR_CHAT_PROTECTION_ENABLED = 170,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
@@ -241,12 +246,6 @@ export enum C {
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
- DC_STR_PROTECTION_DISABLED = 89,
- DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
- DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
- DC_STR_PROTECTION_ENABLED = 88,
- DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
- DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
@@ -306,6 +305,7 @@ export const EventId2EventName: { [key: number]: string } = {
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
2015: 'DC_EVENT_MSG_READ',
+ 2016: 'DC_EVENT_MSG_DELETED',
2020: 'DC_EVENT_CHAT_MODIFIED',
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
2030: 'DC_EVENT_CONTACTS_CHANGED',
diff --git a/node/lib/context.ts b/node/lib/context.ts
index 4c926ba78..b292e9e35 100644
--- a/node/lib/context.ts
+++ b/node/lib/context.ts
@@ -36,7 +36,7 @@ export class Context extends EventEmitter {
}
}
- /** Opens a stanalone context (without an account manager)
+ /** Opens a standalone context (without an account manager)
* automatically starts the event handler */
static open(cwd: string): Context {
const dbFile = join(cwd, 'db.sqlite')
@@ -699,23 +699,6 @@ export class Context extends EventEmitter {
)
}
- /**
- *
- * @param chatId
- * @param protect
- * @returns success boolean
- */
- setChatProtection(chatId: number, protect: boolean) {
- debug(`setChatProtection ${chatId} ${protect}`)
- return Boolean(
- binding.dcn_set_chat_protection(
- this.dcn_context,
- Number(chatId),
- protect ? 1 : 0
- )
- )
- }
-
getChatEphemeralTimer(chatId: number): number {
debug(`getChatEphemeralTimer ${chatId}`)
return binding.dcn_get_chat_ephemeral_timer(
diff --git a/node/lib/deltachat.ts b/node/lib/deltachat.ts
index 2526b9378..dbf002e46 100644
--- a/node/lib/deltachat.ts
+++ b/node/lib/deltachat.ts
@@ -21,12 +21,15 @@ export class AccountManager extends EventEmitter {
accountDir: string
jsonRpcStarted = false
- constructor(cwd: string, os = 'deltachat-node') {
+ constructor(cwd: string, writable = true) {
super()
debug('DeltaChat constructor')
this.accountDir = cwd
- this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
+ this.dcn_accounts = binding.dcn_accounts_new(
+ this.accountDir,
+ writable ? 1 : 0
+ )
}
getAllAccountIds() {
diff --git a/node/src/module.c b/node/src/module.c
index 3d3045017..5c675020e 100644
--- a/node/src/module.c
+++ b/node/src/module.c
@@ -1399,18 +1399,6 @@ NAPI_METHOD(dcn_set_chat_name) {
NAPI_RETURN_INT32(result);
}
-NAPI_METHOD(dcn_set_chat_protection) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_INT32(protect, 1);
-
- int result = dc_set_chat_protection(dcn_context->dc_context,
- chat_id,
- protect);
- NAPI_RETURN_INT32(result);
-}
-
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
NAPI_ARGV(2);
NAPI_DCN_CONTEXT();
@@ -2915,8 +2903,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){
NAPI_METHOD(dcn_accounts_new) {
NAPI_ARGV(2);
- NAPI_ARGV_UTF8_MALLOC(os_name, 0);
- NAPI_ARGV_UTF8_MALLOC(dir, 1);
+ NAPI_ARGV_UTF8_MALLOC(dir, 0);
+ NAPI_ARGV_INT32(writable, 1);
TRACE("calling..");
dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
@@ -2925,7 +2913,7 @@ NAPI_METHOD(dcn_accounts_new) {
}
- dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir);
+ dcn_accounts->dc_accounts = dc_accounts_new(dir, writable);
napi_value result;
NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
@@ -3491,7 +3479,6 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_msg);
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
- NAPI_EXPORT_FUNCTION(dcn_set_chat_protection);
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);
diff --git a/node/test/test.js b/node/test/test.js
index 3c1baed01..2c7912584 100644
--- a/node/test/test.js
+++ b/node/test/test.js
@@ -446,7 +446,8 @@ describe('Offline Tests with unconfigured account', function () {
context.setChatProfileImage(chatId, imagePath)
const blobPath = context.getChat(chatId).getProfileImage()
expect(blobPath.startsWith(blobs)).to.be.true
- expect(blobPath.endsWith(image)).to.be.true
+ expect(blobPath.includes('image')).to.be.true
+ expect(blobPath.endsWith('.jpeg')).to.be.true
context.setChatProfileImage(chatId, null)
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
diff --git a/package.json b/package.json
index 6716b8c01..f3093f89c 100644
--- a/package.json
+++ b/package.json
@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
- "version": "1.112.6"
+ "version": "1.126.1"
}
diff --git a/python/LICENSE b/python/LICENSE
index 14e2f777f..ee6256cdb 100644
--- a/python/LICENSE
+++ b/python/LICENSE
@@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
diff --git a/python/README.rst b/python/README.rst
index da1b25c7e..c5f048912 100644
--- a/python/README.rst
+++ b/python/README.rst
@@ -74,7 +74,9 @@ Developing the bindings
If you want to develop or debug the bindings,
you can create a testing development environment using `tox`::
- tox -c python --devenv env
+ export DCC_RS_DEV="$PWD"
+ export DCC_RS_TARGET=debug
+ tox -c python --devenv env -e py
. env/bin/activate
Inside this environment the bindings are installed
diff --git a/python/doc/api.rst b/python/doc/api.rst
index 049bc2928..f19306391 100644
--- a/python/doc/api.rst
+++ b/python/doc/api.rst
@@ -2,34 +2,34 @@
high level API reference
========================
-- :class:`deltachat.account.Account` (your main entry point, creates the
+- :class:`deltachat.Account` (your main entry point, creates the
other classes)
-- :class:`deltachat.contact.Contact`
-- :class:`deltachat.chat.Chat`
-- :class:`deltachat.message.Message`
+- :class:`deltachat.Contact`
+- :class:`deltachat.Chat`
+- :class:`deltachat.Message`
Account
-------
-.. autoclass:: deltachat.account.Account
+.. autoclass:: deltachat.Account
:members:
Contact
-------
-.. autoclass:: deltachat.contact.Contact
+.. autoclass:: deltachat.Contact
:members:
Chat
----
-.. autoclass:: deltachat.chat.Chat
+.. autoclass:: deltachat.Chat
:members:
Message
-------
-.. autoclass:: deltachat.message.Message
+.. autoclass:: deltachat.Message
:members:
diff --git a/python/examples/group_tracking.py b/python/examples/group_tracking.py
index 7ca003c0d..51f63cb74 100644
--- a/python/examples/group_tracking.py
+++ b/python/examples/group_tracking.py
@@ -32,25 +32,13 @@ class GroupTrackingPlugin:
@account_hookimpl
def ac_member_added(self, chat, contact, actor, message):
- print(
- "ac_member_added {} to chat {} from {}".format(
- contact.addr,
- chat.id,
- actor or message.get_sender_contact().addr,
- ),
- )
+ print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):
- print(
- "ac_member_removed {} from chat {} by {}".format(
- contact.addr,
- chat.id,
- actor or message.get_sender_contact().addr,
- ),
- )
+ print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
def main(argv=None):
diff --git a/python/examples/test_examples.py b/python/examples/test_examples.py
index 6e4f6ef25..ca8e48b69 100644
--- a/python/examples/test_examples.py
+++ b/python/examples/test_examples.py
@@ -24,8 +24,6 @@ def test_echo_quit_plugin(acfactory, lp):
lp.sec("creating a temp account to contact the bot")
(ac1,) = acfactory.get_online_accounts(1)
- botproc.await_resync()
-
lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr)
bot_chat = bot_contact.create_chat()
@@ -54,8 +52,6 @@ def test_group_tracking_plugin(acfactory, lp):
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))
- botproc.await_resync()
-
lp.sec("creating bot test group with bot")
bot_contact = ac1.create_contact(botproc.addr)
ch = ac1.create_group_chat("bot test group")
diff --git a/python/mypy.ini b/python/mypy.ini
index 6b7560ab9..3eb9ae8c8 100644
--- a/python/mypy.ini
+++ b/python/mypy.ini
@@ -24,3 +24,5 @@ ignore_missing_imports = True
[mypy-imap_tools.*]
ignore_missing_imports = True
+[mypy-distutils.*]
+ignore_missing_imports = True
diff --git a/python/pyproject.toml b/python/pyproject.toml
index fb846cc8f..5e0bee124 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -11,10 +11,11 @@ authors = [
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
]
classifiers = [
- "Development Status :: 4 - Beta",
+ "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Programming Language :: Python :: 3",
+ "Topic :: Communications :: Chat",
"Topic :: Communications :: Email",
"Topic :: Software Development :: Libraries",
]
@@ -33,6 +34,7 @@ dynamic = [
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
"Documentation" = "https://py.delta.chat/"
+"Mastodon" = "https://chaos.social/@delta"
[project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin"
diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py
index 6d067f4fb..69268e18c 100644
--- a/python/src/deltachat/account.py
+++ b/python/src/deltachat/account.py
@@ -6,7 +6,7 @@ from array import array
from contextlib import contextmanager
from email.utils import parseaddr
from threading import Event
-from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
+from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union
from . import const, hookspec
from .capi import ffi, lib
@@ -195,7 +195,7 @@ class Account:
assert res != ffi.NULL, f"config value not found for: {name!r}"
return from_dc_charpointer(res)
- def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
+ def _preconfigure_keypair(self, addr: str, secret: str) -> None:
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
@@ -203,7 +203,7 @@ class Account:
res = lib.dc_preconfigure_keypair(
self._dc_context,
as_dc_charpointer(addr),
- as_dc_charpointer(public),
+ ffi.NULL,
as_dc_charpointer(secret),
)
if res == 0:
@@ -427,7 +427,7 @@ class Account:
assert dc_chatlist != ffi.NULL
chatlist = []
- for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
+ for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)):
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
chatlist.append(Chat(self, chat_id))
return chatlist
@@ -617,18 +617,18 @@ class Account:
# meta API for start/stop and event based processing
#
- def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
- from .events import FFIEventLogger
-
+ def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None):
"""get the account running, configure it if necessary. add plugins if provided.
:param addr: the email address of the account
:param password: the password of the account
:param account_plugins: a list of plugins to add
:param show_ffi: show low level ffi events
+ :param displayname: the display name of the account
"""
+ from .events import FFIEventLogger
+
if show_ffi:
- self.set_config("displayname", "bot")
log = FFIEventLogger(self)
self.add_account_plugin(log)
@@ -644,6 +644,8 @@ class Account:
configtracker = self.configure()
configtracker.wait_finish()
+ if displayname:
+ self.set_config("displayname", displayname)
# start IO threads and configure if necessary
self.start_io()
diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py
index f49b91fbc..e28a31178 100644
--- a/python/src/deltachat/contact.py
+++ b/python/src/deltachat/contact.py
@@ -71,13 +71,16 @@ class Contact:
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False)
- def is_verified(self):
+ def is_verified(self) -> bool:
"""Return True if the contact is verified."""
- return lib.dc_contact_is_verified(self._dc_contact)
+ return lib.dc_contact_is_verified(self._dc_contact) == 2
- def get_verifier(self, contact):
+ def get_verifier(self, contact) -> Optional["Contact"]:
"""Return the address of the contact that verified the contact."""
- return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
+ verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
+ if verifier_id == 0:
+ return None
+ return Contact(self.account, verifier_id)
def get_profile_image(self) -> Optional[str]:
"""Get contact profile image.
diff --git a/python/src/deltachat/cutil.py b/python/src/deltachat/cutil.py
index ad9810219..f907166b9 100644
--- a/python/src/deltachat/cutil.py
+++ b/python/src/deltachat/cutil.py
@@ -15,7 +15,7 @@ def as_dc_charpointer(obj):
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
- for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
+ for i in range(lib.dc_array_get_cnt(dc_array_t)):
yield constructor(lib.dc_array_get_id(dc_array_t, i))
diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py
index 0ae86a9ad..4c4ec5119 100644
--- a/python/src/deltachat/events.py
+++ b/python/src/deltachat/events.py
@@ -9,11 +9,11 @@ from contextlib import contextmanager
from queue import Empty, Queue
from . import const
+from .account import Account
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
-from .account import Account
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py
index e58d97d05..3f813e52d 100644
--- a/python/src/deltachat/message.py
+++ b/python/src/deltachat/message.py
@@ -486,6 +486,9 @@ class Message:
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
return lib.dc_msg_get_download_state(dc_msg)
+ def download_full(self) -> None:
+ lib.dc_download_full_msg(self.account._dc_context, self.id)
+
# some code for handling DC_MSG_* view types
@@ -507,8 +510,7 @@ def get_viewtype_code_from_name(view_type_name):
if code is not None:
return code
raise ValueError(
- "message typecode not found for {!r}, "
- "available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
+ f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
)
diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py
index 192afb508..944982475 100644
--- a/python/src/deltachat/testplugin.py
+++ b/python/src/deltachat/testplugin.py
@@ -9,7 +9,7 @@ import threading
import time
import weakref
from queue import Queue
-from typing import Callable, List, Optional, Dict, Set
+from typing import Callable, Dict, List, Optional, Set
import pytest
import requests
@@ -137,6 +137,9 @@ def pytest_report_header(config, startdir):
@pytest.fixture(scope="session")
def testprocess(request):
+ """Return live account configuration manager.
+
+ The returned object is a :class:`TestProcess` object."""
return TestProcess(pytestconfig=request.config)
@@ -231,6 +234,8 @@ def write_dict_to_dir(dic, target_dir):
@pytest.fixture()
def data(request):
+ """Test data."""
+
class Data:
def __init__(self) -> None:
# trying to find test data heuristically
@@ -473,10 +478,9 @@ class ACFactory:
except IndexError:
pass
else:
- fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
- if fname_pub and fname_sec:
- account._preconfigure_keypair(addr, fname_pub, fname_sec)
+ if fname_sec:
+ account._preconfigure_keypair(addr, fname_sec)
return True
print(f"WARN: could not use preconfigured keys for {addr!r}")
@@ -614,6 +618,7 @@ class ACFactory:
@pytest.fixture()
def acfactory(request, tmpdir, testprocess, data):
+ """Account factory."""
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
yield am
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
@@ -676,21 +681,17 @@ class BotProcess:
print("+++IGN:", line)
ignored.append(line)
- def await_resync(self):
- self.fnmatch_lines(
- """
- *Resync: collected * message IDs in folder INBOX*
- """,
- )
-
@pytest.fixture()
def tmp_db_path(tmpdir):
+ """Return a path inside the temporary directory where the database can be created."""
return tmpdir.join("test.db").strpath
@pytest.fixture()
def lp():
+ """Log printer fixture."""
+
class Printer:
def sec(self, msg: str) -> None:
print()
diff --git a/python/src/deltachat/tracker.py b/python/src/deltachat/tracker.py
index f8fbc3f8f..1fa88044c 100644
--- a/python/src/deltachat/tracker.py
+++ b/python/src/deltachat/tracker.py
@@ -1,6 +1,6 @@
from queue import Queue
from threading import Event
-from typing import List, TYPE_CHECKING
+from typing import TYPE_CHECKING, List
from .hookspec import Global, account_hookimpl
diff --git a/python/tests/bench_test_setup.py b/python/tests/bench_test_setup.py
index 5dc5e3877..ef750550f 100644
--- a/python/tests/bench_test_setup.py
+++ b/python/tests/bench_test_setup.py
@@ -15,6 +15,6 @@ class TestEmpty:
def test_prepare_setup_measurings(self, acfactory):
acfactory.get_online_accounts(BENCH_NUM)
- @pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
+ @pytest.mark.parametrize("num", range(BENCH_NUM + 1))
def test_setup_online_accounts(self, acfactory, num):
acfactory.get_online_accounts(num)
diff --git a/python/tests/stress_test_db.py b/python/tests/stress_test_db.py
index 43a04116a..9a793a816 100644
--- a/python/tests/stress_test_db.py
+++ b/python/tests/stress_test_db.py
@@ -8,7 +8,7 @@ import pytest
import deltachat
-def test_db_busy_error(acfactory, tmpdir):
+def test_db_busy_error(acfactory):
starttime = time.time()
log_lock = threading.RLock()
diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py
index 736c0d1dd..75c554a6d 100644
--- a/python/tests/test_0_complex_or_slow.py
+++ b/python/tests/test_0_complex_or_slow.py
@@ -1,6 +1,7 @@
import sys
import pytest
+import deltachat as dc
class TestGroupStressTests:
@@ -136,6 +137,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: read member added message")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
+ assert msg.is_system_message()
assert "added" in msg.text.lower()
lp.sec("ac1: send message")
@@ -149,9 +151,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
- # If we verified the contact ourselves then verifier addr == contact addr
ac2_ac1_contact = ac2.get_contacts()[0]
- assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
+ assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
lp.sec("ac2: send message and let ac1 read it")
chat2.send_text("world")
@@ -176,14 +177,15 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
ac2_ac1_contact = ac2.get_contacts()[0]
- assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
+ assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
ac2_ac3_contact = ac2.get_contacts()[1]
- assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
+ assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
- # Skip system message about added member
- ac3._evtracker.wait_next_incoming_message()
+ # System message about the added member.
+ msg = ac3._evtracker.wait_next_incoming_message()
+ assert msg.is_system_message()
msg = ac3._evtracker.wait_next_incoming_message()
assert msg.text == "hi"
assert msg.is_encrypted()
@@ -494,7 +496,7 @@ def test_multidevice_sync_seen(acfactory, lp):
assert "Expires: " in ac1_clone_message.get_message_info()
-def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
+def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
"""The test for the bug #3836:
- Alice has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
@@ -507,9 +509,10 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
for ac in [ac1, ac1_offl]:
ac.set_config("bcc_self", "1")
acfactory.bring_accounts_online()
- dir = tmpdir.mkdir("exportdir")
- ac1.export_self_keys(dir.strpath)
- ac1_offl.import_self_keys(dir.strpath)
+ dir = tmp_path / "exportdir"
+ dir.mkdir()
+ ac1.export_self_keys(str(dir))
+ ac1_offl.import_self_keys(str(dir))
ac1_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
@@ -523,7 +526,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
lp.sec("ac2: sending message")
# Message can be sent only after a receipt of "vg-member-added" message. Just wait for
# "Member Me () added by ." message.
- ac2._evtracker.wait_next_incoming_message()
+ msg_in = ac2._evtracker.wait_next_incoming_message()
+ assert msg_in.is_system_message()
msg_out = chat2.send_text("hello")
lp.sec("ac1: receiving message")
@@ -538,10 +542,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
assert msg_in.text == msg_out.text
assert msg_in.get_sender_contact().addr == ac2_addr
- ac1.set_config("bcc_self", "0")
-
-def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
+def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
"""Another test for the bug #3836:
- Bob has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
@@ -556,9 +558,10 @@ def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
acfactory.bring_accounts_online()
- dir = tmpdir.mkdir("exportdir")
- ac2.export_self_keys(dir.strpath)
- ac2_offl.import_self_keys(dir.strpath)
+ dir = tmp_path / "exportdir"
+ dir.mkdir()
+ ac2.export_self_keys(str(dir))
+ ac2_offl.import_self_keys(str(dir))
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
@@ -587,4 +590,67 @@ def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert msg_in.text == msg_out.text
- ac2.set_config("bcc_self", "0")
+
+def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
+ """Test for the issue #4346:
+ - User is added to a verified group.
+ - First device of the user downloads "member added" from the group.
+ - First device removes "member added" from the server.
+ - Some new messages are sent to the group.
+ - Second device comes online, receives these new messages. The result is a verified group with unverified members.
+ - First device re-gossips Autocrypt keys to the group.
+ - Now the seconds device has all members verified.
+ """
+ ac1, ac2 = acfactory.get_online_accounts(2)
+ ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
+ for ac in [ac2, ac2_offl]:
+ ac.set_config("bcc_self", "1")
+ ac2.set_config("delete_server_after", "1")
+ ac2.set_config("gossip_period", "0") # Re-gossip in every message
+ acfactory.bring_accounts_online()
+ dir = tmp_path / "exportdir"
+ dir.mkdir()
+ ac2.export_self_keys(str(dir))
+ ac2_offl.import_self_keys(str(dir))
+ ac2_offl.stop_io()
+
+ lp.sec("ac1: create verified-group QR, ac2 scans and joins")
+ chat1 = ac1.create_group_chat("hello", verified=True)
+ assert chat1.is_protected()
+ qr = chat1.get_join_qr()
+ lp.sec("ac2: start QR-code based join-group protocol")
+ chat2 = ac2.qr_join_chat(qr)
+ ac1._evtracker.wait_securejoin_inviter_progress(1000)
+ # Wait for "Member Me () added by ." message.
+ msg_in = ac2._evtracker.wait_next_incoming_message()
+ assert msg_in.is_system_message()
+
+ lp.sec("ac2: waiting for 'member added' to be deleted on the server")
+ ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
+
+ lp.sec("ac1: sending 'hi' to the group")
+ ac2.set_config("delete_server_after", "0")
+ chat1.send_text("hi")
+
+ lp.sec("ac2_offl: going online, checking the 'hi' message")
+ ac2_offl.start_io()
+ msg_in = ac2_offl._evtracker.wait_next_incoming_message()
+ assert not msg_in.is_system_message()
+ assert msg_in.text.startswith("[The message was sent with non-verified encryption")
+ ac2_offl_ac1_contact = msg_in.get_sender_contact()
+ assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
+ assert not ac2_offl_ac1_contact.is_verified()
+ chat2_offl = msg_in.chat
+ assert chat2_offl.is_protected()
+
+ lp.sec("ac2: sending message re-gossiping Autocrypt keys")
+ chat2.send_text("hi2")
+
+ lp.sec("ac2_offl: receiving message")
+ ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
+ msg_in = ac2_offl.get_message_by_id(ev.data2)
+ assert not msg_in.is_system_message()
+ assert msg_in.text == "hi2"
+ assert msg_in.chat == chat2_offl
+ assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
+ assert ac2_offl_ac1_contact.is_verified()
diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py
index 85e1774bd..431463952 100644
--- a/python/tests/test_1_online.py
+++ b/python/tests/test_1_online.py
@@ -2,18 +2,18 @@ import os
import queue
import sys
import time
+import base64
from datetime import datetime, timezone
import pytest
from imap_tools import AND, U
-from deltachat import const
-from deltachat.hookspec import account_hookimpl
-from deltachat.message import Message
+import deltachat as dc
+from deltachat import account_hookimpl, Message, Chat
from deltachat.tracker import ImexTracker
-def test_basic_imap_api(acfactory, tmpdir):
+def test_basic_imap_api(acfactory, tmp_path):
ac1, ac2 = acfactory.get_online_accounts(2)
chat12 = acfactory.get_accepted_chat(ac1, ac2)
@@ -28,7 +28,7 @@ def test_basic_imap_api(acfactory, tmpdir):
imap2.mark_all_read()
assert imap2.get_unread_cnt() == 0
- imap2.dump_imap_structures(tmpdir, logfile=sys.stdout)
+ imap2.dump_imap_structures(tmp_path, logfile=sys.stdout)
imap2.shutdown()
@@ -36,8 +36,8 @@ def test_basic_imap_api(acfactory, tmpdir):
def test_configure_generate_key(acfactory, lp):
# A slow test which will generate new keys.
acfactory.remove_preconfigured_keys()
- ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048))
- ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519))
+ ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
+ ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
acfactory.bring_accounts_online()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -72,35 +72,37 @@ def test_configure_canceled(acfactory):
pass
-def test_configure_unref(tmpdir):
+def test_configure_unref(tmp_path):
"""Test that removing the last reference to the context during ongoing configuration
does not result in use-after-free."""
from deltachat.capi import ffi, lib
- path = tmpdir.mkdir("test_configure_unref").join("dc.db").strpath
- dc_context = lib.dc_context_new(ffi.NULL, path.encode("utf8"), ffi.NULL)
+ path = tmp_path / "test_configure_unref"
+ path.mkdir()
+ dc_context = lib.dc_context_new(ffi.NULL, str(path / "dc.db").encode("utf8"), ffi.NULL)
lib.dc_set_config(dc_context, "addr".encode("utf8"), "foo@x.testrun.org".encode("utf8"))
lib.dc_set_config(dc_context, "mail_pw".encode("utf8"), "abc".encode("utf8"))
lib.dc_configure(dc_context)
lib.dc_context_unref(dc_context)
-def test_export_import_self_keys(acfactory, tmpdir, lp):
+def test_export_import_self_keys(acfactory, tmp_path, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
- dir = tmpdir.mkdir("exportdir")
- export_files = ac1.export_self_keys(dir.strpath)
+ dir = tmp_path / "exportdir"
+ dir.mkdir()
+ export_files = ac1.export_self_keys(str(dir))
assert len(export_files) == 2
for x in export_files:
- assert x.startswith(dir.strpath)
+ assert x.startswith(str(dir))
(key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
ac1._evtracker.consume_events()
lp.sec("exported keys (private and public)")
- for name in os.listdir(dir.strpath):
- lp.indent(dir.strpath + os.sep + name)
+ for name in dir.iterdir():
+ lp.indent(str(dir / name))
lp.sec("importing into existing account")
- ac2.import_self_keys(dir.strpath)
+ ac2.import_self_keys(str(dir))
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
assert key_id2 == key_id
@@ -156,62 +158,65 @@ def test_one_account_send_bcc_setting(acfactory, lp):
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
-def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp):
+def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
- basename = "somedäüta.html.zip"
- p = os.path.join(tmpdir.strpath, basename)
- with open(p, "w") as f:
- f.write("some data")
+ basename = "somedäüta"
+ ext = ".html.zip"
+ p = tmp_path / (basename + ext)
+ p.write_text("some data")
def send_and_receive_message():
lp.sec("ac1: prepare and send attachment + text to ac2")
msg1 = Message.new_empty(ac1, "file")
msg1.set_text("withfile")
- msg1.set_file(p)
+ msg1.set_file(str(p))
chat.send_msg(msg1)
lp.sec("ac2: receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
- assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
+ assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
return ac2.get_message_by_id(ev.data2)
msg = send_and_receive_message()
assert msg.text == "withfile"
assert open(msg.filename).read() == "some data"
- assert msg.filename.endswith(basename)
+ msg.filename.index(basename)
+ assert msg.filename.endswith(ext)
msg2 = send_and_receive_message()
assert msg2.text == "withfile"
assert open(msg2.filename).read() == "some data"
- assert msg2.filename.endswith("html.zip")
+ msg2.filename.index(basename)
+ assert msg2.filename.endswith(ext)
assert msg.filename != msg2.filename
-def test_send_file_html_attachment(tmpdir, acfactory, lp):
+def test_send_file_html_attachment(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
- basename = "test.html"
+ basename = "test"
+ ext = ".html"
content = "textdata"
- p = os.path.join(tmpdir.strpath, basename)
- with open(p, "w") as f:
- # write wrong html to see if core tries to parse it
- # (it shouldn't as it's a file attachment)
- f.write(content)
+ p = tmp_path / (basename + ext)
+ # write wrong html to see if core tries to parse it
+ # (it shouldn't as it's a file attachment)
+ p.write_text(content)
lp.sec("ac1: prepare and send attachment + text to ac2")
- chat.send_file(p, mime_type="text/html")
+ chat.send_file(str(p), mime_type="text/html")
lp.sec("ac2: receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
- assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
+ assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
msg = ac2.get_message_by_id(ev.data2)
assert open(msg.filename).read() == content
- assert msg.filename.endswith(basename)
+ msg.filename.index(basename)
+ assert msg.filename.endswith(ext)
def test_html_message(acfactory, lp):
@@ -324,6 +329,59 @@ def test_webxdc_message(acfactory, data, lp):
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
+def test_webxdc_huge_update(acfactory, data, lp):
+ ac1, ac2 = acfactory.get_online_accounts(2)
+ chat = ac1.create_chat(ac2)
+
+ msg1 = Message.new_empty(ac1, "webxdc")
+ msg1.set_text("message1")
+ msg1.set_file(data.get_path("webxdc/minimal.xdc"))
+ msg1 = chat.send_msg(msg1)
+ assert msg1.is_webxdc()
+ assert msg1.filename
+
+ msg2 = ac2._evtracker.wait_next_incoming_message()
+ assert msg2.is_webxdc()
+
+ payload = "A" * 1000
+ assert msg1.send_status_update({"payload": payload}, "some test data")
+ ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
+ update = msg2.get_status_updates()[0]
+ assert update["payload"] == payload
+
+
+def test_webxdc_download_on_demand(acfactory, data, lp):
+ ac1, ac2 = acfactory.get_online_accounts(2)
+ acfactory.introduce_each_other([ac1, ac2])
+ chat = acfactory.get_accepted_chat(ac1, ac2)
+
+ msg1 = Message.new_empty(ac1, "webxdc")
+ msg1.set_text("message1")
+ msg1.set_file(data.get_path("webxdc/minimal.xdc"))
+ msg1 = chat.send_msg(msg1)
+ assert msg1.is_webxdc()
+ assert msg1.filename
+
+ msg2 = ac2._evtracker.wait_next_incoming_message()
+ assert msg2.is_webxdc()
+
+ lp.sec("ac2 sets download limit")
+ ac2.set_config("download_limit", "100")
+ assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
+ ac2_update = ac2._evtracker.wait_next_incoming_message()
+ assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
+ assert not msg2.get_status_updates()
+
+ ac2_update.download_full()
+ ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
+ assert msg2.get_status_updates()
+
+ # Get a event notifying that the message disappeared from the chat.
+ msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
+ assert msgs_changed_event.data1 == msg2.chat.id
+ assert msgs_changed_event.data2 == 0
+
+
def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
@@ -351,7 +409,42 @@ def test_move_works(acfactory):
# Message is downloaded
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
- assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
+ assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
+
+
+def test_move_avoids_loop(acfactory):
+ """Test that the message is only moved once.
+
+ This is to avoid busy loop if moved message reappears in the Inbox
+ or some scanned folder later.
+ For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
+ so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
+ We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
+ """
+ ac1 = acfactory.new_online_configuring_account()
+ ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
+ acfactory.bring_accounts_online()
+ ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
+ ac1_chat.send_text("Message 1")
+
+ # Message is moved to the DeltaChat folder and downloaded.
+ ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
+ assert ac2_msg1.text == "Message 1"
+
+ # Move the message to the INBOX again.
+ ac2.direct_imap.select_folder("DeltaChat")
+ ac2.direct_imap.conn.move(["*"], "INBOX")
+
+ ac1_chat.send_text("Message 2")
+ ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
+ assert ac2_msg2.text == "Message 2"
+
+ # Check that Message 1 is still in the INBOX folder
+ # and Message 2 is in the DeltaChat folder.
+ ac2.direct_imap.select_folder("INBOX")
+ assert len(ac2.direct_imap.get_all_messages()) == 1
+ ac2.direct_imap.select_folder("DeltaChat")
+ assert len(ac2.direct_imap.get_all_messages()) == 1
def test_move_works_on_self_sent(acfactory):
@@ -396,9 +489,12 @@ def test_forward_messages(acfactory, lp):
lp.sec("ac2: check new chat has a forwarded message")
assert chat3.is_promoted()
messages = chat3.get_messages()
+ assert len(messages) == 1
msg = messages[-1]
assert msg.is_forwarded()
ac2.delete_messages(messages)
+ ev = ac2._evtracker.get_matching("DC_EVENT_MSG_DELETED")
+ assert ev.data2 == messages[0].id
assert not chat3.get_messages()
@@ -531,8 +627,8 @@ def test_send_and_receive_message_markseen(acfactory, lp):
lp.step("1")
for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
- assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
- assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
+ assert ev.data1 > dc.const.DC_CHAT_ID_LAST_SPECIAL
+ assert ev.data2 > dc.const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
# Check that ac1 marks the read receipt as read.
@@ -703,7 +799,7 @@ def test_mdn_asymmetric(acfactory, lp):
assert len(chat.get_messages()) == 1
# Wait for the message to be marked as seen on IMAP.
- ac1._evtracker.get_info_contains("Marked messages 1 in folder DeltaChat as seen.")
+ ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
# MDN is received even though MDNs are already disabled
assert msg_out.is_out_mdn_received()
@@ -1204,7 +1300,7 @@ def test_quote_encrypted(acfactory, lp):
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
-def test_quote_attachment(tmpdir, acfactory, lp):
+def test_quote_attachment(tmp_path, acfactory, lp):
"""Test that replies with an attachment and a quote are received correctly."""
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1219,15 +1315,14 @@ def test_quote_attachment(tmpdir, acfactory, lp):
assert received_message.text == "hi"
basename = "attachment.txt"
- p = os.path.join(tmpdir.strpath, basename)
- with open(p, "w") as f:
- f.write("data to send")
+ p = tmp_path / basename
+ p.write_text("data to send")
lp.sec("ac2 sends a reply to ac1")
chat2 = received_message.create_chat()
reply = Message.new_empty(ac2, "file")
reply.set_text("message reply")
- reply.set_file(p)
+ reply.set_file(str(p))
reply.quote = received_message
chat2.send_msg(reply)
@@ -1334,7 +1429,7 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in
-def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
+def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
@@ -1369,23 +1464,27 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
lp.sec("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
- path = tmpdir.join("large")
- with open(path, "wb") as fout:
- fout.write(os.urandom(download_limit + 1))
- msgs.append(chat.send_file(path.strpath))
-
- lp.sec("sending a reaction to the large message from ac1 to ac2")
- react_str = "\N{THUMBS UP SIGN}"
- msgs.append(msgs[-1].send_reaction(react_str))
-
+ path = tmp_path / "large"
+ path.write_bytes(os.urandom(download_limit + 1))
+ msgs.append(chat.send_file(str(path)))
for m in msgs:
ac1._evtracker.wait_msg_delivered(m)
+
+ lp.sec("sending a reaction to the large message from ac1 to ac2")
+ # TODO: Find the reason of an occasional message reordering on the server (so that the reaction
+ # has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
+ # have a later INTERNALDATE.
+ time.sleep(1.1)
+ react_str = "\N{THUMBS UP SIGN}"
+ msgs.append(msgs[-1].send_reaction(react_str))
+ ac1._evtracker.wait_msg_delivered(msgs[-1])
+
ac2.start_io()
lp.sec("wait for ac2 to receive a reaction")
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.get_sender_contact().addr == ac1_addr
- assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
+ assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert reactions_queue.get() == msg2
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
@@ -1411,7 +1510,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
ac1._evtracker.wait_msg_delivered(msg1)
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
- time.sleep(2)
+ time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
@@ -1431,7 +1530,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
assert reactions.get_by_contact(contacts[0]) == react_str
-def test_import_export_online_all(acfactory, tmpdir, data, lp):
+def test_import_export_online_all(acfactory, tmp_path, data, lp):
(ac1,) = acfactory.get_online_accounts(1)
lp.sec("create some chat content")
@@ -1443,10 +1542,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
chat1.send_image(original_image_path)
# Add another 100KB file that ensures that the progress is smooth enough
- path = tmpdir.join("attachment.txt")
- with open(path, "w") as file:
+ path = tmp_path / "attachment.txt"
+ with path.open("w") as file:
file.truncate(100000)
- chat1.send_file(path.strpath)
+ chat1.send_file(str(path))
def assert_account_is_proper(ac):
contacts = ac.get_contacts(query="some1")
@@ -1464,12 +1563,13 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
assert_account_is_proper(ac1)
- backupdir = tmpdir.mkdir("backup")
+ backupdir = tmp_path / "backup"
+ backupdir.mkdir()
lp.sec(f"export all to {backupdir}")
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
ac1.stop_io()
- ac1.imex(backupdir.strpath, const.DC_IMEX_EXPORT_BACKUP)
+ ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
# check progress events for export
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
@@ -1487,7 +1587,7 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
ac2 = acfactory.get_unconfigured_account()
lp.sec("get latest backup file")
- path2 = ac2.get_latest_backupfile(backupdir.strpath)
+ path2 = ac2.get_latest_backupfile(str(backupdir))
assert path2 == path
lp.sec("import backup and check it's proper")
@@ -1505,10 +1605,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
lp.sec(f"Second-time export all to {backupdir}")
ac1.stop_io()
- path2 = ac1.export_all(backupdir.strpath)
+ path2 = ac1.export_all(str(backupdir))
assert os.path.exists(path2)
assert path2 != path
- assert ac2.get_latest_backupfile(backupdir.strpath) == path2
+ assert ac2.get_latest_backupfile(str(backupdir)) == path2
def test_ac_setup_message(acfactory, lp):
@@ -1569,8 +1669,12 @@ def test_qr_setup_contact(acfactory, lp):
ac1._evtracker.wait_securejoin_inviter_progress(1000)
-def test_qr_join_chat(acfactory, lp):
+@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
+def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
ac1, ac2 = acfactory.get_online_accounts(2)
+ ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
+ ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
+
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
@@ -1583,6 +1687,78 @@ def test_qr_join_chat(acfactory, lp):
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac1._evtracker.wait_securejoin_inviter_progress(1000)
+ msg = ac2._evtracker.wait_next_incoming_message()
+ assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
+
+ # ac1 reloads the chat.
+ chat = Chat(chat.account, chat.id)
+ assert not chat.is_protected()
+
+ # ac2 reloads the chat.
+ ch = Chat(ch.account, ch.id)
+ assert not ch.is_protected()
+
+
+def test_qr_new_group_unblocked(acfactory, lp):
+ """Regression test for a bug intoduced in core v1.113.0.
+ ac2 scans a verified group QR code created by ac1.
+ This results in creation of a blocked 1:1 chat with ac1 on ac2,
+ but ac1 contact is not blocked on ac2.
+ Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
+ ac2 should receive a message and create a contact request for the group.
+ Due to a bug previously ac2 created a blocked group.
+ """
+
+ ac1, ac2 = acfactory.get_online_accounts(2)
+ ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
+ qr = ac1_chat.get_join_qr()
+ ac2.qr_join_chat(qr)
+
+ ac1._evtracker.wait_securejoin_inviter_progress(1000)
+
+ ac1_new_chat = ac1.create_group_chat("Another group")
+ ac1_new_chat.add_contact(ac2)
+ # Receive "Member added" message.
+ ac2._evtracker.wait_next_incoming_message()
+
+ ac1_new_chat.send_text("Hello!")
+ ac2_msg = ac2._evtracker.wait_next_incoming_message()
+ assert ac2_msg.text == "Hello!"
+ assert ac2_msg.chat.is_contact_request()
+
+
+def test_qr_email_capitalization(acfactory, lp):
+ """Regression test for a bug
+ that resulted in failure to propagate verification via gossip in a verified group
+ when the database already contained the contact with a different email address capitalization.
+ """
+
+ ac1, ac2, ac3 = acfactory.get_online_accounts(3)
+
+ # ac1 adds ac2 as a contact with an email address in uppercase.
+ ac2_addr_uppercase = ac2.get_config("addr").upper()
+ lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
+ ac1.create_contact(ac2_addr_uppercase)
+
+ lp.sec("ac3 creates a verified group with a QR code")
+ chat = ac3.create_group_chat("hello", verified=True)
+ qr = chat.get_join_qr()
+
+ lp.sec("ac1 joins a verified group via a QR code")
+ ac1_chat = ac1.qr_join_chat(qr)
+ msg = ac1._evtracker.wait_next_incoming_message()
+ assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
+ assert len(ac1_chat.get_contacts()) == 2
+
+ lp.sec("ac2 joins a verified group via a QR code")
+ ac2.qr_join_chat(qr)
+ ac1._evtracker.wait_next_incoming_message()
+
+ # ac1 should see both ac3 and ac2 as verified.
+ assert len(ac1_chat.get_contacts()) == 3
+ for contact in ac1_chat.get_contacts():
+ assert contact.is_verified()
+
def test_set_get_contact_avatar(acfactory, data, lp):
lp.sec("configuring ac1 and ac2")
@@ -1768,13 +1944,15 @@ def test_set_get_group_image(acfactory, data, lp):
lp.sec("ac1: add ac2 to promoted group chat")
chat.add_contact(ac2) # sends one message
+ lp.sec("ac2: wait for receiving member added message from ac1")
+ msg1 = ac2._evtracker.wait_next_incoming_message()
+ assert msg1.is_system_message() # Member added
+
lp.sec("ac1: send a first message to ac2")
chat.send_text("hi") # sends another message
assert chat.is_promoted()
lp.sec("ac2: wait for receiving message from ac1")
- msg1 = ac2._evtracker.wait_next_incoming_message()
- assert msg1.is_system_message() # Member added
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "hi"
assert msg1.chat.id == msg2.chat.id
@@ -1800,15 +1978,15 @@ def test_connectivity(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("scan_all_folders_debounce_secs", "0")
- ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
+ ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test stop_io() and start_io()")
ac1.stop_io()
- ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
+ ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
- ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
- ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
+ 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_CONNECTED)
lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
@@ -1829,8 +2007,8 @@ def test_connectivity(acfactory, lp):
ac2.create_chat(ac1).send_text("Hi 2")
- ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
- ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
+ ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTED, dc.const.DC_CONNECTIVITY_WORKING)
+ ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2
@@ -1840,7 +2018,7 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
while 1:
- assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
+ assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -1855,7 +2033,7 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
while 1:
- assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
+ assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -1864,10 +2042,10 @@ def test_connectivity(acfactory, lp):
ac1.set_config("configured_mail_pw", "abc")
ac1.stop_io()
- ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
+ ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
- ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
- ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
+ ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
+ ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_fetch_deleted_msg(acfactory, lp):
@@ -2350,9 +2528,9 @@ def test_archived_muted_chat(acfactory, lp):
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
while 1:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
- if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
+ if ev.data1 == dc.const.DC_CHAT_ID_ARCHIVED_LINK:
assert ev.data2 == 0
- archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
+ archive = ac2.get_chat_by_id(dc.const.DC_CHAT_ID_ARCHIVED_LINK)
assert archive.count_fresh_messages() == 1
assert chat2.count_fresh_messages() == 1
break
diff --git a/python/tests/test_2_increation.py b/python/tests/test_2_increation.py
index c7e35119a..2f053e002 100644
--- a/python/tests/test_2_increation.py
+++ b/python/tests/test_2_increation.py
@@ -30,28 +30,28 @@ def wait_msgs_changed(account, msgs_list):
class TestOnlineInCreation:
- def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
+ 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 tmpdir.strpath != ac1.get_blobdir()
- src = tmpdir.join("file.txt").ensure(file=1)
+ assert str(tmp_path) != ac1.get_blobdir()
+ src = tmp_path / "file.txt"
+ src.touch()
with pytest.raises(Exception):
- chat.prepare_message_file(src.strpath)
+ chat.prepare_message_file(str(src))
- def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
+ 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 tmpdir.strpath != ac1.get_blobdir()
- src = tmpdir.join("file.txt")
- src.write("hello there\n")
- chat.send_file(src.strpath)
-
- blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
- assert os.path.exists(blob_src), "file.txt not copied to 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)
diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py
index e9a297681..4b040c65a 100644
--- a/python/tests/test_3_offline.py
+++ b/python/tests/test_3_offline.py
@@ -4,12 +4,11 @@ from datetime import datetime, timedelta, timezone
import pytest
-from deltachat import Account, const
+import deltachat as dc
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
-from deltachat.hookspec import account_hookimpl
-from deltachat.message import Message
from deltachat.tracker import ImexFailed
+from deltachat import Account, account_hookimpl, Message
@pytest.mark.parametrize(
@@ -52,26 +51,25 @@ def test_parse_system_add_remove(msgtext, res):
class TestOfflineAccountBasic:
- def test_wrong_db(self, tmpdir):
- p = tmpdir.join("hello.db")
- p.write("123")
- account = Account(p.strpath)
+ def test_wrong_db(self, tmp_path):
+ p = tmp_path / "hello.db"
+ p.write_text("123")
+ account = Account(str(p))
assert not account.is_open()
- def test_os_name(self, tmpdir):
- p = tmpdir.join("hello.db")
+ def test_os_name(self, tmp_path):
+ p = tmp_path / "hello.db"
# we can't easily test if os_name is used in X-Mailer
# outgoing messages without a full Online test
# but we at least check Account accepts the arg
- ac1 = Account(p.strpath, os_name="solarpunk")
+ ac1 = Account(str(p), os_name="solarpunk")
ac1.get_info()
def test_preconfigure_keypair(self, acfactory, data):
ac = acfactory.get_unconfigured_account()
- alice_public = data.read_path("key/alice-public.asc")
alice_secret = data.read_path("key/alice-secret.asc")
- assert alice_public and alice_secret
- ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
+ assert alice_secret
+ ac._preconfigure_keypair("alice@example.org", alice_secret)
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
@@ -299,13 +297,13 @@ class TestOfflineChat:
assert not d["draft"] if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
- ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
+ ac1.set_stock_translation(dc.const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1._evtracker.consume_events()
with pytest.raises(ValueError):
- ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
+ ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
- ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
+ ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(500, "xyz %1$s")
@@ -481,6 +479,19 @@ class TestOfflineChat:
contact2 = ac1.create_contact("display1 ", "real")
assert contact2.name == "real"
+ def test_send_lots_of_offline_msgs(self, acfactory):
+ ac1 = acfactory.get_pseudo_configured_account()
+ ac1.set_config("configured_mail_server", "example.org")
+ ac1.set_config("configured_mail_user", "example.org")
+ ac1.set_config("configured_mail_pw", "example.org")
+ ac1.set_config("configured_send_server", "example.org")
+ ac1.set_config("configured_send_user", "example.org")
+ ac1.set_config("configured_send_pw", "example.org")
+ ac1.start_io()
+ chat = ac1.create_contact("some1@example.org", name="some1").create_chat()
+ for i in range(50):
+ chat.send_text("hello")
+
def test_create_chat_simple(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
@@ -496,22 +507,22 @@ class TestOfflineChat:
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
- def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
- backupdir = tmpdir.mkdir("backup")
+ def test_import_export_on_unencrypted_acct(self, acfactory, tmp_path):
+ backupdir = tmp_path / "backup"
+ backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 ").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
- bin = tmpdir.join("some.bin")
- with bin.open("w") as f:
- f.write("\00123" * 10000)
- msg = chat.send_file(bin.strpath)
+ bin = tmp_path / "some.bin"
+ bin.write_bytes(b"\00123" * 10000)
+ msg = chat.send_file(str(bin))
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
- assert not backupdir.listdir()
+ assert not list(backupdir.iterdir())
ac1.stop_io()
- path = ac1.export_all(backupdir.strpath)
+ path = ac1.export_all(str(backupdir))
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
ac2.import_all(path)
@@ -525,27 +536,27 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
- def test_import_export_on_encrypted_acct(self, acfactory, tmpdir):
+ def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
passphrase1 = "passphrase1"
passphrase2 = "passphrase2"
- backupdir = tmpdir.mkdir("backup")
+ backupdir = tmp_path / "backup"
+ backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
chat = ac1.create_contact("some1 ").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
- bin = tmpdir.join("some.bin")
- with bin.open("w") as f:
- f.write("\00123" * 10000)
- msg = chat.send_file(bin.strpath)
+ bin = tmp_path / "some.bin"
+ bin.write_bytes(b"\00123" * 10000)
+ msg = chat.send_file(str(bin))
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
- assert not backupdir.listdir()
+ assert not list(backupdir.iterdir())
ac1.stop_io()
- path = ac1.export_all(backupdir.strpath)
+ path = ac1.export_all(str(backupdir))
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account(closed=True)
@@ -580,27 +591,27 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
- def test_import_export_with_passphrase(self, acfactory, tmpdir):
+ def test_import_export_with_passphrase(self, acfactory, tmp_path):
passphrase = "test_passphrase"
wrong_passphrase = "wrong_passprase"
- backupdir = tmpdir.mkdir("backup")
+ backupdir = tmp_path / "backup"
+ backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 ").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
- bin = tmpdir.join("some.bin")
- with bin.open("w") as f:
- f.write("\00123" * 10000)
- msg = chat.send_file(bin.strpath)
+ bin = tmp_path / "some.bin"
+ bin.write_bytes(b"\00123" * 10000)
+ msg = chat.send_file(str(bin))
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
- assert not backupdir.listdir()
+ assert not list(backupdir.iterdir())
ac1.stop_io()
- path = ac1.export_all(backupdir.strpath, passphrase)
+ path = ac1.export_all(str(backupdir), passphrase)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
@@ -619,7 +630,7 @@ class TestOfflineChat:
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
- def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmpdir):
+ def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
"""
Test that account passphrase isn't lost if backup failed to be imported.
See https://github.com/deltachat/deltachat-core-rust/issues/3379
@@ -627,24 +638,24 @@ class TestOfflineChat:
acct_passphrase = "passphrase1"
bak_passphrase = "passphrase2"
wrong_passphrase = "wrong_passprase"
- backupdir = tmpdir.mkdir("backup")
+ backupdir = tmp_path / "backup"
+ backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 ").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
- bin = tmpdir.join("some.bin")
- with bin.open("w") as f:
- f.write("\00123" * 10000)
- msg = chat.send_file(bin.strpath)
+ bin = tmp_path / "some.bin"
+ bin.write_bytes(b"\00123" * 10000)
+ msg = chat.send_file(str(bin))
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
- assert not backupdir.listdir()
+ assert not list(backupdir.iterdir())
ac1.stop_io()
- path = ac1.export_all(backupdir.strpath, bak_passphrase)
+ path = ac1.export_all(str(backupdir), bak_passphrase)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account(closed=True)
@@ -805,7 +816,7 @@ class TestOfflineChat:
lp.sec("check message count of only system messages (without daymarkers)")
dc_array = ffi.gc(
- lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
+ lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
lib.dc_array_unref,
)
assert len(list(iter_array(dc_array, lambda x: x))) == 2
diff --git a/python/tests/test_4_lowlevel.py b/python/tests/test_4_lowlevel.py
index 84a751ea4..23ef3dbe3 100644
--- a/python/tests/test_4_lowlevel.py
+++ b/python/tests/test_4_lowlevel.py
@@ -1,7 +1,8 @@
-import os
+import json
from queue import Queue
-from deltachat import capi, const, cutil, register_global_plugin
+import deltachat as dc
+from deltachat import capi, cutil, register_global_plugin
from deltachat.capi import ffi, lib
from deltachat.hookspec import global_hookimpl
from deltachat.testplugin import (
@@ -9,6 +10,7 @@ from deltachat.testplugin import (
create_dict_from_files_in_path,
write_dict_to_dir,
)
+from deltachat.cutil import from_optional_dc_charpointer
# from deltachat.account import EventLogger
@@ -64,16 +66,17 @@ class TestACSetup:
assert pc._account2state[ac1] == pc.IDLEREADY
assert pc._account2state[ac2] == pc.IDLEREADY
- def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir):
+ def test_store_and_retrieve_configured_account_cache(self, acfactory, tmp_path):
ac1 = acfactory.get_pseudo_configured_account()
holder = acfactory._acsetup.testprocess
assert holder.cache_maybe_store_configured_db_files(ac1)
assert not holder.cache_maybe_store_configured_db_files(ac1)
- acdir = tmpdir.mkdir("newaccount")
+ acdir = tmp_path / "newaccount"
+ acdir.mkdir()
addr = ac1.get_config("addr")
- target_db_path = acdir.join("db").strpath
- assert holder.cache_maybe_retrieve_configured_db_files(addr, target_db_path)
- assert len(os.listdir(acdir)) >= 2
+ target_db_path = acdir / "db"
+ assert holder.cache_maybe_retrieve_configured_db_files(addr, str(target_db_path))
+ assert sum(1 for _ in acdir.iterdir()) >= 2
def test_liveconfig_caching(acfactory, monkeypatch):
@@ -111,40 +114,40 @@ def test_dc_close_events(acfactory):
shutdowns.get(timeout=2)
-def test_wrong_db(tmpdir):
- p = tmpdir.join("hello.db")
+def test_wrong_db(tmp_path):
+ p = tmp_path / "hello.db"
# write an invalid database file
- p.write("x123" * 10)
+ p.write_bytes(b"x123" * 10)
- context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
+ context = lib.dc_context_new(ffi.NULL, str(p).encode("ascii"), ffi.NULL)
assert not lib.dc_context_is_open(context)
-def test_empty_blobdir(tmpdir):
- db_fname = tmpdir.join("hello.db")
+def test_empty_blobdir(tmp_path):
+ db_fname = tmp_path / "hello.db"
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
- lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
+ lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), b""),
lib.dc_context_unref,
)
assert ctx != ffi.NULL
def test_event_defines():
- assert const.DC_EVENT_INFO == 100
- assert const.DC_CONTACT_ID_SELF
+ assert dc.const.DC_EVENT_INFO == 100
+ assert dc.const.DC_CONTACT_ID_SELF
def test_sig():
sig = capi.lib.dc_event_has_string_data
- assert not sig(const.DC_EVENT_MSGS_CHANGED)
- assert sig(const.DC_EVENT_INFO)
- assert sig(const.DC_EVENT_WARNING)
- assert sig(const.DC_EVENT_ERROR)
- assert sig(const.DC_EVENT_SMTP_CONNECTED)
- assert sig(const.DC_EVENT_IMAP_CONNECTED)
- assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
- assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
+ assert not sig(dc.const.DC_EVENT_MSGS_CHANGED)
+ assert sig(dc.const.DC_EVENT_INFO)
+ assert sig(dc.const.DC_EVENT_WARNING)
+ assert sig(dc.const.DC_EVENT_ERROR)
+ assert sig(dc.const.DC_EVENT_SMTP_CONNECTED)
+ assert sig(dc.const.DC_EVENT_IMAP_CONNECTED)
+ assert sig(dc.const.DC_EVENT_SMTP_MESSAGE_SENT)
+ assert sig(dc.const.DC_EVENT_IMEX_FILE_WRITTEN)
def test_markseen_invalid_message_ids(acfactory):
@@ -173,10 +176,10 @@ def test_provider_info_none():
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
-def test_get_info_open(tmpdir):
- db_fname = tmpdir.join("test.db")
+def test_get_info_open(tmp_path):
+ db_fname = tmp_path / "test.db"
ctx = ffi.gc(
- lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
+ lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
@@ -215,3 +218,36 @@ def test_logged_ac_process_ffi_failure(acfactory):
assert "ac_process_ffi_event" in res
assert "ZeroDivisionError" in res
assert "Traceback" in res
+
+
+def test_jsonrpc_blocking_call(tmp_path):
+ accounts_fname = tmp_path / "accounts"
+ writable = True
+ accounts = ffi.gc(
+ lib.dc_accounts_new(str(accounts_fname).encode("ascii"), writable),
+ lib.dc_accounts_unref,
+ )
+ jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
+ res = json.loads(
+ from_optional_dc_charpointer(
+ lib.dc_jsonrpc_blocking_call(
+ jsonrpc,
+ json.dumps(
+ {"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
+ ).encode("utf-8"),
+ ),
+ ),
+ )
+ assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
+
+ res = json.loads(
+ from_optional_dc_charpointer(
+ lib.dc_jsonrpc_blocking_call(
+ jsonrpc,
+ json.dumps(
+ {"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
+ ).encode("utf-8"),
+ ),
+ ),
+ )
+ assert res == {"jsonrpc": "2.0", "id": "456", "result": False}
diff --git a/python/tox.ini b/python/tox.ini
index 99fd0c703..ed6069dec 100644
--- a/python/tox.ini
+++ b/python/tox.ini
@@ -25,6 +25,9 @@ deps =
pytest-xdist
pdbpp
requests
+# urllib3 2.0 does not work in manylinux2014 containers.
+# https://github.com/deltachat/deltachat-core-rust/issues/4788
+ urllib3<2
[testenv:.pkg]
passenv =
@@ -59,7 +62,8 @@ commands =
[testenv:doc]
changedir=doc
deps =
- sphinx
+# Pinned due to incompatibility of breathe with sphinx 7.2:
+ sphinx<=7.1.2
breathe
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
diff --git a/release-date.in b/release-date.in
index 5dc1be6ba..666f14451 100644
--- a/release-date.in
+++ b/release-date.in
@@ -1 +1 @@
-2023-04-04
\ No newline at end of file
+2023-10-24
\ No newline at end of file
diff --git a/release.toml b/release.toml
deleted file mode 100644
index 21181de6c..000000000
--- a/release.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-pre-release-commit-message = "chore({{crate_name}}): release {{version}}"
-pro-release-commit-message = "chore({{crate_name}}): starting development cycle for {{next_version}}"
-no-dev-version = true
\ No newline at end of file
diff --git a/scripts/README.md b/scripts/README.md
index 6b2baac37..551d7e77a 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -10,6 +10,8 @@ and an own build machine.
- `deny.sh` runs `cargo deny` for all Rust code in the project.
+- `codespell.sh` spellchecks the source code using `codespell` tool.
+
- `../.github/workflows` contains jobs run by GitHub Actions.
- `remote_tests_python.sh` rsyncs to a build machine and runs
@@ -18,6 +20,10 @@ and an own build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
`run-rust-test.sh` remotely on the build machine.
+- `make-python-testenv.sh` creates local python test development environment.
+ Reusing the same environment is faster than running `run-python-test.sh` which always
+ recreates environment from scratch and runs additional lints.
+
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
- `run_all.sh` builds Python wheels
diff --git a/scripts/concourse/README.md b/scripts/concourse/README.md
index b022d9bc4..ad1882f41 100644
--- a/scripts/concourse/README.md
+++ b/scripts/concourse/README.md
@@ -12,10 +12,15 @@ where `secret.yml` contains the following secrets:
```
c.delta.chat:
private_key: |
- -----BEGIN RSA PRIVATE KEY-----
+ -----BEGIN OPENSSH PRIVATE KEY-----
...
- -----END RSA PRIVATE KEY-----
+ -----END OPENSSH PRIVATE KEY-----
devpi:
login: dc
password: ...
```
+
+Secrets can be read from the password manager:
+```
+fly -t b1 set-pipeline -c docs_wheels.yml -p docs_wheels -l <(pass show delta/b1.delta.chat/secret.yml)
+```
diff --git a/scripts/concourse/docs_wheels.yml b/scripts/concourse/docs_wheels.yml
index 3f461ad20..cc65d00fd 100644
--- a/scripts/concourse/docs_wheels.yml
+++ b/scripts/concourse/docs_wheels.yml
@@ -153,11 +153,13 @@ jobs:
- -ec
- |
apt-get update -y
- apt-get install -y --no-install-recommends python3-pip python3-setuptools
- pip3 install devpi
- devpi use https://m.devpi.net/dc/master
- devpi login ((devpi.login)) --password ((devpi.password))
- devpi upload py-wheels/*manylinux201*
+ apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
+ python3 -m venv env
+ env/bin/pip install --upgrade pip
+ env/bin/pip install devpi
+ env/bin/devpi use https://m.devpi.net/dc/master
+ env/bin/devpi login ((devpi.login)) --password ((devpi.password))
+ env/bin/devpi upload py-wheels/*manylinux201*
- name: python-aarch64
plan:
@@ -223,11 +225,13 @@ jobs:
- -ec
- |
apt-get update -y
- apt-get install -y --no-install-recommends python3-pip python3-setuptools
- pip3 install devpi
- devpi use https://m.devpi.net/dc/master
- devpi login ((devpi.login)) --password ((devpi.password))
- devpi upload py-wheels/*manylinux201*
+ apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
+ python3 -m venv env
+ env/bin/pip install --upgrade pip
+ env/bin/pip install devpi
+ env/bin/devpi use https://m.devpi.net/dc/master
+ env/bin/devpi login ((devpi.login)) --password ((devpi.password))
+ env/bin/devpi upload py-wheels/*manylinux201*
- name: python-musl-x86_64
plan:
@@ -293,11 +297,13 @@ jobs:
- -ec
- |
apt-get update -y
- apt-get install -y --no-install-recommends python3-pip python3-setuptools
- pip3 install devpi
- devpi use https://m.devpi.net/dc/master
- devpi login ((devpi.login)) --password ((devpi.password))
- devpi upload py-wheels/*musllinux_1_1_x86_64*
+ apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
+ python3 -m venv env
+ env/bin/pip install --upgrade pip
+ env/bin/pip install devpi
+ env/bin/devpi use https://m.devpi.net/dc/master
+ env/bin/devpi login ((devpi.login)) --password ((devpi.password))
+ env/bin/devpi upload py-wheels/*musllinux_1_1_x86_64*
- name: python-musl-aarch64
plan:
@@ -363,8 +369,10 @@ jobs:
- -ec
- |
apt-get update -y
- apt-get install -y --no-install-recommends python3-pip python3-setuptools
- pip3 install devpi
- devpi use https://m.devpi.net/dc/master
- devpi login ((devpi.login)) --password ((devpi.password))
- devpi upload py-wheels/*musllinux_1_1_aarch64*
+ apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
+ python3 -m venv env
+ env/bin/pip install --upgrade pip
+ env/bin/pip install devpi
+ env/bin/devpi use https://m.devpi.net/dc/master
+ env/bin/devpi login ((devpi.login)) --password ((devpi.password))
+ env/bin/devpi upload py-wheels/*musllinux_1_1_aarch64*
diff --git a/scripts/coredeps/Dockerfile b/scripts/coredeps/Dockerfile
index a6ceed532..55b90d9f6 100644
--- a/scripts/coredeps/Dockerfile
+++ b/scripts/coredeps/Dockerfile
@@ -6,3 +6,4 @@ FROM $BASEIMAGE
RUN pipx install tox
COPY install-rust.sh /scripts/
RUN /scripts/install-rust.sh
+RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi
diff --git a/scripts/coredeps/install-rust.sh b/scripts/coredeps/install-rust.sh
index 8181cc991..20e72611b 100755
--- a/scripts/coredeps/install-rust.sh
+++ b/scripts/coredeps/install-rust.sh
@@ -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.68.0
+RUST_VERSION=1.72.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
diff --git a/scripts/create-provider-data-rs.py b/scripts/create-provider-data-rs.py
index 824e3354d..589cd0b90 100755
--- a/scripts/create-provider-data-rs.py
+++ b/scripts/create-provider-data-rs.py
@@ -44,7 +44,7 @@ def file2url(f):
def process_opt(data):
if not "opt" in data:
- return "Default::default()"
+ return "ProviderOptions::new()"
opt = "ProviderOptions {\n"
opt_data = data.get("opt", "")
for key in opt_data:
@@ -54,7 +54,7 @@ def process_opt(data):
if value in {"True", "False"}:
value = value.lower()
opt += " " + key + ": " + value + ",\n"
- opt += " ..Default::default()\n"
+ opt += " ..ProviderOptions::new()\n"
opt += " }"
return opt
@@ -62,7 +62,7 @@ def process_opt(data):
def process_config_defaults(data):
if not "config_defaults" in data:
return "None"
- defaults = "Some(vec![\n"
+ defaults = "Some(&[\n"
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
@@ -96,11 +96,11 @@ def process_data(data, file):
raise TypeError("domain used twice: " + domain)
domains_set.add(domain)
- domains += ' ("' + domain + '", &*' + file2varname(file) + "),\n"
+ domains += ' ("' + domain + '", &' + file2varname(file) + "),\n"
comment += domain + ", "
ids = ""
- ids += ' ("' + file2id(file) + '", &*' + file2varname(file) + "),\n"
+ ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n"
server = ""
has_imap = False
@@ -155,18 +155,18 @@ def process_data(data, file):
provider += (
"static "
+ file2varname(file)
- + ": Lazy = Lazy::new(|| Provider {\n"
+ + ": Provider = Provider {\n"
)
provider += ' id: "' + file2id(file) + '",\n'
provider += " status: Status::" + status.capitalize() + ",\n"
provider += ' before_login_hint: "' + before_login_hint + '",\n'
provider += ' after_login_hint: "' + after_login_hint + '",\n'
provider += ' overview_page: "' + file2url(file) + '",\n'
- provider += " server: vec![\n" + server + " ],\n"
+ provider += " server: &[\n" + server + " ],\n"
provider += " opt: " + opt + ",\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
- provider += "});\n\n"
+ provider += "};\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
diff --git a/scripts/deny.sh b/scripts/deny.sh
index 1b6c9ac0b..05c508b23 100755
--- a/scripts/deny.sh
+++ b/scripts/deny.sh
@@ -1,2 +1,6 @@
#!/bin/sh
+
+# Update package cache without changing the lockfile.
+cargo update --dry-run
+
cargo deny --workspace --all-features check -D warnings
diff --git a/scripts/make-python-testenv.sh b/scripts/make-python-testenv.sh
new file mode 100755
index 000000000..ce3d0b71e
--- /dev/null
+++ b/scripts/make-python-testenv.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+#
+# Script to create or update a python development environment.
+# It rebuilds the core and bindings as needed.
+#
+# After running the script, you can either
+# run `pytest` directly with `venv/bin/pytest python/`
+# or activate the environment with `. venv/bin/activate`
+# and run `pytest` from there.
+set -euo pipefail
+
+export DCC_RS_TARGET=debug
+export DCC_RS_DEV="$PWD"
+cargo build -p deltachat_ffi --features jsonrpc
+
+tox -c python -e py --devenv venv
+venv/bin/pip install --upgrade pip
diff --git a/scripts/run_all.sh b/scripts/run_all.sh
index 87ce41ea3..42edb094f 100755
--- a/scripts/run_all.sh
+++ b/scripts/run_all.sh
@@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
-tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
+tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
diff --git a/scripts/set_core_version.py b/scripts/set_core_version.py
index cd6303cc9..0958c1669 100755
--- a/scripts/set_core_version.py
+++ b/scripts/set_core_version.py
@@ -7,6 +7,7 @@ import pathlib
import re
import subprocess
from argparse import ArgumentParser
+from pathlib import Path
rex = re.compile(r'version = "(\S+)"')
@@ -95,22 +96,14 @@ def main():
today = datetime.date.today().isoformat()
if "alpha" not in newversion:
- changelog_name = "CHANGELOG.md"
- changelog_tmpname = changelog_name + ".tmp"
- changelog_tmp = open(changelog_tmpname, "w")
found = False
- for line in open(changelog_name):
- ## 1.25.0
- if line == f"## [{newversion}]\n":
- line = f"## [{newversion}] - {today}\n"
+ for line in Path("CHANGELOG.md").open():
+ if line == f"## [{newversion}] - {today}\n":
found = True
- changelog_tmp.write(line)
if not found:
raise SystemExit(
f"{changelog_name} contains no entry for version: {newversion}"
)
- changelog_tmp.close()
- os.rename(changelog_tmpname, changelog_name)
for toml_filename in toml_list:
replace_toml_version(toml_filename, newversion)
@@ -128,11 +121,15 @@ def main():
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
- print("after commit, on master make sure to: ")
- print("")
+ print("After commit, make sure to:")
+ print()
print(f" git tag -a v{newversion}")
print(f" git push origin v{newversion}")
- print("")
+ print(f" gh release create v{newversion} -n ''")
+ print()
+ print("Merge release branch into `master` if the release")
+ print("is made on a stable branch.")
+ print()
if __name__ == "__main__":
diff --git a/scripts/wheel-rpc-server.py b/scripts/wheel-rpc-server.py
new file mode 100755
index 000000000..41f97af95
--- /dev/null
+++ b/scripts/wheel-rpc-server.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+"""Build Python wheels for deltachat-rpc-server.
+Run scripts/zig-rpc-server.sh first."""
+from pathlib import Path
+from wheel.wheelfile import WheelFile
+import tomllib
+import tarfile
+from io import BytesIO
+
+
+def metadata_contents(version):
+ return f"""Metadata-Version: 2.1
+Name: deltachat-rpc-server
+Version: {version}
+Summary: Delta Chat JSON-RPC server
+"""
+
+
+def build_source_package(version):
+ filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
+
+ with tarfile.open(filename, "w:gz") as pkg:
+
+ def pack(name, contents):
+ contents = contents.encode()
+ tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}")
+ tar_info.mode = 0o644
+ tar_info.size = len(contents)
+ pkg.addfile(tar_info, BytesIO(contents))
+
+ pack("PKG-INFO", metadata_contents(version))
+ pack(
+ "pyproject.toml",
+ f"""[build-system]
+requires = ["setuptools==68.2.2", "pip"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "deltachat-rpc-server"
+version = "{version}"
+
+[project.scripts]
+deltachat-rpc-server = "deltachat_rpc_server:main"
+""",
+ )
+ pack(
+ "setup.py",
+ f"""
+import sys
+from setuptools import setup, find_packages
+from distutils.cmd import Command
+from setuptools.command.install import install
+from setuptools.command.build import build
+import subprocess
+import platform
+import tempfile
+from zipfile import ZipFile
+from pathlib import Path
+import shutil
+
+
+class BuildCommand(build):
+ def run(self):
+ tmpdir = tempfile.mkdtemp()
+ subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "pip",
+ "download",
+ "--no-input",
+ "--timeout",
+ "1000",
+ "--platform",
+ "musllinux_1_1_" + platform.machine(),
+ "--only-binary=:all:",
+ "deltachat-rpc-server=={version}",
+ ],
+ cwd=tmpdir,
+ )
+
+ wheel_path = next(Path(tmpdir).glob("*.whl"))
+ with ZipFile(wheel_path, "r") as wheel:
+ exe_path = wheel.extract("deltachat_rpc_server/deltachat-rpc-server", "src")
+ Path(exe_path).chmod(0o700)
+ wheel.extract("deltachat_rpc_server/__init__.py", "src")
+
+ shutil.rmtree(tmpdir)
+ return super().run()
+
+
+setup(
+ cmdclass={{"build": BuildCommand}},
+ package_data={{"deltachat_rpc_server": ["deltachat-rpc-server"]}},
+)
+""",
+ )
+ pack("src/deltachat_rpc_server/__init__.py", "")
+
+
+def build_wheel(version, binary, tag, windows=False):
+ filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
+
+ with WheelFile(filename, "w") as wheel:
+ wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
+ wheel.write("deltachat-rpc-server/README.md", "deltachat_rpc_server/README.md")
+ if windows:
+ wheel.writestr(
+ "deltachat_rpc_server/__init__.py",
+ """import os, sys, subprocess
+def main():
+ argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server.exe"), *sys.argv[1:]]
+ sys.exit(subprocess.call(argv))
+""",
+ )
+ else:
+ wheel.writestr(
+ "deltachat_rpc_server/__init__.py",
+ """import os, sys
+def main():
+ argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server"), *sys.argv[1:]]
+ os.execv(argv[0], argv)
+""",
+ )
+
+ Path(binary).chmod(0o755)
+ wheel.write(
+ binary,
+ "deltachat_rpc_server/deltachat-rpc-server.exe"
+ if windows
+ else "deltachat_rpc_server/deltachat-rpc-server",
+ )
+ wheel.writestr(
+ f"deltachat_rpc_server-{version}.dist-info/METADATA",
+ metadata_contents(version),
+ )
+ wheel.writestr(
+ f"deltachat_rpc_server-{version}.dist-info/WHEEL",
+ "Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: {tag}",
+ )
+ wheel.writestr(
+ f"deltachat_rpc_server-{version}.dist-info/entry_points.txt",
+ "[console_scripts]\ndeltachat-rpc-server = deltachat_rpc_server:main",
+ )
+
+
+def main():
+ with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
+ cargo_toml = tomllib.load(f)
+ version = cargo_toml["package"]["version"]
+ Path("dist").mkdir(exist_ok=True)
+ build_source_package(version)
+ build_wheel(
+ version,
+ "dist/deltachat-rpc-server-x86_64-linux",
+ "py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
+ )
+ build_wheel(
+ version,
+ "dist/deltachat-rpc-server-armv7-linux",
+ "py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
+ )
+ build_wheel(
+ version,
+ "dist/deltachat-rpc-server-aarch64-linux",
+ "py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
+ )
+ build_wheel(
+ version,
+ "dist/deltachat-rpc-server-i686-linux",
+ "py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
+ )
+
+ # macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
+ build_wheel(
+ version,
+ "dist/deltachat-rpc-server-x86_64-macos",
+ "py3-none-macosx_10_7_x86_64",
+ )
+ build_wheel(
+ version,
+ "dist/deltachat-rpc-server-aarch64-macos",
+ "py3-none-macosx_11_0_arm64",
+ )
+
+ build_wheel(
+ version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
+ )
+ build_wheel(
+ version,
+ "dist/deltachat-rpc-server-win64.exe",
+ "py3-none-win_amd64",
+ windows=True,
+ )
+
+
+main()
diff --git a/scripts/zig-cc b/scripts/zig-cc
index 4b1293dde..ffa3278b4 100755
--- a/scripts/zig-cc
+++ b/scripts/zig-cc
@@ -1,10 +1,20 @@
#!/usr/bin/env python
+# /// pyproject
+# [run]
+# dependencies = [
+# "ziglang==0.11.0"
+# ]
+# ///
+import os
import subprocess
import sys
-import os
def flag_filter(flag: str) -> bool:
+ # Workaround for .
+ if flag == "-latomic":
+ return False
+
if flag == "-lc":
return False
if flag == "-Wl,-melf_i386":
@@ -24,8 +34,23 @@ def main():
else:
zig_cpu_args = []
+ # Disable atomics and use locks instead in OpenSSL.
+ # Zig toolchains do not provide atomics.
+ # This is a workaround for
+ args += ["-DBROKEN_CLANG_ATOMICS"]
+
subprocess.run(
- ["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True
+ [
+ sys.executable,
+ "-m",
+ "ziglang",
+ "cc",
+ "-target",
+ zig_target,
+ *zig_cpu_args,
+ *args,
+ ],
+ check=True,
)
diff --git a/scripts/zig-rpc-server.sh b/scripts/zig-rpc-server.sh
index e411b20bb..455c0f150 100755
--- a/scripts/zig-rpc-server.sh
+++ b/scripts/zig-rpc-server.sh
@@ -8,15 +8,7 @@ set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
-export RUSTUP_TOOLCHAIN=1.68.1
-
-ZIG_VERSION=0.11.0-dev.2213+515e1c93e
-
-# Download Zig
-rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
-wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
-tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
-export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
+export RUSTUP_TOOLCHAIN=1.72.0
rustup target add i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
@@ -50,3 +42,9 @@ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
+
+mkdir -p dist
+cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
+cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
+cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
+cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux
diff --git a/spec.md b/spec.md
index b12a75464..ea34862aa 100644
--- a/spec.md
+++ b/spec.md
@@ -43,7 +43,7 @@ the `Subject` header SHOULD be `Message from `.
Replies to messages MAY follow the typical `Re:`-format.
The body MAY contain text which MUST have the content type `text/plain`
-or `mulipart/alternative` containing `text/plain`.
+or `multipart/alternative` containing `text/plain`.
The text MAY be divided into a user-text-part and a footer-part using the
line `-- ` (minus, minus, space, lineend).
diff --git a/src/accounts.rs b/src/accounts.rs
index 082057595..7cbe1cea4 100644
--- a/src/accounts.rs
+++ b/src/accounts.rs
@@ -7,6 +7,9 @@ use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
+use tokio::sync::oneshot;
+use tokio::task::JoinHandle;
+use tokio::time::{sleep, Duration};
use uuid::Uuid;
use crate::context::Context;
@@ -18,6 +21,7 @@ use crate::stock_str::StockStrings;
pub struct Accounts {
dir: PathBuf,
config: Config,
+ /// Map from account ID to the account.
accounts: BTreeMap,
/// Event channel to emit account manager errors.
@@ -32,16 +36,16 @@ pub struct Accounts {
impl Accounts {
/// Loads or creates an accounts folder at the given `dir`.
- pub async fn new(dir: PathBuf) -> Result {
- if !dir.exists() {
+ pub async fn new(dir: PathBuf, writable: bool) -> Result {
+ if writable && !dir.exists() {
Accounts::create(&dir).await?;
}
- Accounts::open(dir).await
+ Accounts::open(dir, writable).await
}
/// Creates a new default structure.
- pub async fn create(dir: &Path) -> Result<()> {
+ async fn create(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
.await
.context("failed to create folder")?;
@@ -53,13 +57,13 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
- pub async fn open(dir: PathBuf) -> Result {
+ async fn open(dir: PathBuf, writable: bool) -> Result {
ensure!(dir.exists(), "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
- let config = Config::from_file(config_file)
+ let config = Config::from_file(config_file, writable)
.await
.context("failed to load accounts config")?;
let events = Events::new();
@@ -78,12 +82,12 @@ impl Accounts {
})
}
- /// Get an account by its `id`:
+ /// Returns an account by its `id`:
pub fn get_account(&self, id: u32) -> Option {
self.accounts.get(&id).cloned()
}
- /// Get the currently selected account.
+ /// Returns the currently selected account.
pub fn get_selected_account(&self) -> Option {
let id = self.config.get_selected_account();
self.accounts.get(&id).cloned()
@@ -97,14 +101,14 @@ impl Accounts {
}
}
- /// Select the given account.
+ /// Selects the given account.
pub async fn select_account(&mut self, id: u32) -> Result<()> {
self.config.select_account(id).await?;
Ok(())
}
- /// Add a new account and opens it.
+ /// Adds a new account and opens it.
///
/// Returns account ID.
pub async fn add_account(&mut self) -> Result {
@@ -139,7 +143,7 @@ impl Accounts {
Ok(account_config.id)
}
- /// Remove an account.
+ /// Removes an account.
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
let ctx = self
.accounts
@@ -160,7 +164,7 @@ impl Accounts {
Ok(())
}
- /// Migrate an existing account into this structure.
+ /// Migrates an existing account into this structure.
///
/// Returns the ID of new account.
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result {
@@ -295,16 +299,22 @@ impl Accounts {
}
/// Configuration file name.
-pub const CONFIG_NAME: &str = "accounts.toml";
+const CONFIG_NAME: &str = "accounts.toml";
+
+/// Lockfile name.
+const LOCKFILE_NAME: &str = "accounts.lock";
/// Database file name.
-pub const DB_NAME: &str = "dc.db";
+const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug)]
struct Config {
file: PathBuf,
inner: InnerConfig,
+ // We lock the lockfile in the Config constructors to protect also from having multiple Config
+ // objects for the same config file.
+ lock_task: Option>>,
}
/// Account manager configuration file contents.
@@ -318,17 +328,74 @@ struct InnerConfig {
pub accounts: Vec,
}
+impl Drop for Config {
+ fn drop(&mut self) {
+ if let Some(lock_task) = self.lock_task.take() {
+ lock_task.abort();
+ }
+ }
+}
+
impl Config {
- /// Creates a new configuration file in the given account manager directory.
- pub async fn new(dir: &Path) -> Result {
+ /// Creates a new Config for `file`, but doesn't open/sync it.
+ async fn new_nosync(file: PathBuf, lock: bool) -> Result {
+ let dir = file.parent().context("Cannot get config file directory")?;
let inner = InnerConfig {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
};
- let file = dir.join(CONFIG_NAME);
- let mut cfg = Self { file, inner };
+ if !lock {
+ let cfg = Self {
+ file,
+ inner,
+ lock_task: None,
+ };
+ return Ok(cfg);
+ }
+ let lockfile = dir.join(LOCKFILE_NAME);
+ let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
+ let (locked_tx, locked_rx) = oneshot::channel();
+ let lock_task: JoinHandle> = tokio::spawn(async move {
+ let mut timeout = Duration::from_millis(100);
+ let _guard = loop {
+ match lock.try_write() {
+ Ok(guard) => break Ok(guard),
+ Err(err) => {
+ if timeout.as_millis() > 1600 {
+ break Err(err);
+ }
+ // We need to wait for the previous lock_task to be aborted thus unlocking
+ // the lockfile. We don't open configs for writing often outside of the
+ // tests, so this adds delays to the tests, but otherwise ok.
+ sleep(timeout).await;
+ if err.kind() == std::io::ErrorKind::WouldBlock {
+ timeout *= 2;
+ }
+ }
+ }
+ }?;
+ locked_tx
+ .send(())
+ .ok()
+ .context("Cannot notify about lockfile locking")?;
+ let (_tx, rx) = oneshot::channel();
+ rx.await?;
+ Ok(())
+ });
+ let cfg = Self {
+ file,
+ inner,
+ lock_task: Some(lock_task),
+ };
+ locked_rx.await?;
+ Ok(cfg)
+ }
+ /// Creates a new configuration file in the given account manager directory.
+ pub async fn new(dir: &Path) -> Result {
+ let lock = true;
+ let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
cfg.sync().await?;
Ok(cfg)
@@ -338,6 +405,11 @@ impl Config {
/// Takes a mutable reference because the saved file is a part of the `Config` state. This
/// protects from parallel calls resulting to a wrong file contents.
async fn sync(&mut self) -> Result<()> {
+ ensure!(!self
+ .lock_task
+ .as_ref()
+ .context("Config is read-only")?
+ .is_finished());
let tmp_path = self.file.with_extension("toml.tmp");
let mut file = fs::File::create(&tmp_path)
.await
@@ -356,24 +428,28 @@ impl Config {
}
/// Read a configuration from the given file into memory.
- pub async fn from_file(file: PathBuf) -> Result {
- let dir = file.parent().context("can't get config file directory")?;
- let bytes = fs::read(&file).await.context("failed to read file")?;
+ pub async fn from_file(file: PathBuf, writable: bool) -> Result {
+ let dir = file
+ .parent()
+ .context("Cannot get config file directory")?
+ .to_path_buf();
+ let mut config = Self::new_nosync(file, writable).await?;
+ let bytes = fs::read(&config.file)
+ .await
+ .context("Failed to read file")?;
let s = std::str::from_utf8(&bytes)?;
- let mut inner: InnerConfig = toml::from_str(s).context("failed to parse config")?;
+ config.inner = toml::from_str(s).context("Failed to parse config")?;
// Previous versions of the core stored absolute paths in account config.
// Convert them to relative paths.
let mut modified = false;
- for account in &mut inner.accounts {
- if let Ok(new_dir) = account.dir.strip_prefix(dir) {
+ for account in &mut config.inner.accounts {
+ if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
account.dir = new_dir.to_path_buf();
modified = true;
}
}
-
- let mut config = Self { file, inner };
- if modified {
+ if modified && writable {
config.sync().await?;
}
@@ -517,26 +593,44 @@ mod tests {
let p: PathBuf = dir.path().join("accounts1");
{
- let mut accounts = Accounts::new(p.clone()).await.unwrap();
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
accounts.add_account().await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
- {
- let accounts = Accounts::open(p).await.unwrap();
+ for writable in [true, false] {
+ let accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
}
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_account_new_open_conflict() {
+ let dir = tempfile::tempdir().unwrap();
+ let p: PathBuf = dir.path().join("accounts");
+ let writable = true;
+ let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
+
+ let writable = true;
+ assert!(Accounts::new(p.clone(), writable).await.is_err());
+
+ let writable = false;
+ let accounts = Accounts::new(p, writable).await.unwrap();
+ assert_eq!(accounts.accounts.len(), 0);
+ assert_eq!(accounts.config.get_selected_account(), 0);
+ }
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_new_add_remove() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
- let mut accounts = Accounts::new(p.clone()).await.unwrap();
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -563,7 +657,8 @@ mod tests {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts");
- let mut accounts = Accounts::new(p.clone()).await?;
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable).await?;
assert!(accounts.get_selected_account().is_none());
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -584,7 +679,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
- let mut accounts = Accounts::new(p.clone()).await.unwrap();
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -621,7 +717,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
- let mut accounts = Accounts::new(p.clone()).await.unwrap();
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
for expected_id in 1..10 {
let id = accounts.add_account().await.unwrap();
@@ -641,7 +738,8 @@ mod tests {
let dummy_accounts = 10;
let (id0, id1, id2) = {
- let mut accounts = Accounts::new(p.clone()).await?;
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable).await?;
accounts.add_account().await?;
let ids = accounts.get_all();
assert_eq!(ids.len(), 1);
@@ -676,7 +774,8 @@ mod tests {
assert!(id2 > id1 + dummy_accounts);
let (id0_reopened, id1_reopened, id2_reopened) = {
- let accounts = Accounts::new(p.clone()).await?;
+ let writable = false;
+ let accounts = Accounts::new(p.clone(), writable).await?;
let ctx = accounts.get_selected_account().unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
@@ -721,7 +820,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
- let accounts = Accounts::new(p.clone()).await?;
+ let writable = true;
+ let accounts = Accounts::new(p.clone(), writable).await?;
// Make sure there are no accounts.
assert_eq!(accounts.accounts.len(), 0);
@@ -747,7 +847,8 @@ mod tests {
let dir = tempfile::tempdir().context("failed to create tempdir")?;
let p: PathBuf = dir.path().join("accounts");
- let mut accounts = Accounts::new(p.clone())
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable)
.await
.context("failed to create accounts manager")?;
@@ -767,7 +868,8 @@ mod tests {
assert!(passphrase_set_success);
drop(accounts);
- let accounts = Accounts::new(p.clone())
+ let writable = false;
+ let accounts = Accounts::new(p.clone(), writable)
.await
.context("failed to create second accounts manager")?;
let account = accounts
@@ -791,7 +893,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
- let mut accounts = Accounts::new(p.clone()).await?;
+ let writable = true;
+ let mut accounts = Accounts::new(p.clone(), writable).await?;
accounts.add_account().await?;
accounts.add_account().await?;
diff --git a/src/authres.rs b/src/authres.rs
index cc684e544..2192bfbb5 100644
--- a/src/authres.rs
+++ b/src/authres.rs
@@ -357,7 +357,6 @@ mod tests {
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
- use crate::message;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
@@ -705,7 +704,7 @@ Authentication-Results: dkim=";
let received = tcm
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
.await;
- assert!(!received.text.as_ref().unwrap().contains("1234"));
+ assert!(!received.text.contains("1234"));
assert!(received.error.is_some());
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
@@ -786,7 +785,7 @@ Authentication-Results: dkim=";
.insert_str(0, "List-Post: \n");
let rcvd = alice.recv_msg(&sent).await;
assert!(!rcvd.get_showpadlock());
- assert_eq!(&rcvd.text.unwrap(), "hellooo in the mailinglist again");
+ assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(())
}
@@ -825,7 +824,9 @@ Authentication-Results: dkim=";
// Disallowing keychanges is disabled for now:
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
// The message info should contain a warning:
- assert!(message::get_msg_info(&bob, rcvd.id)
+ assert!(rcvd
+ .id
+ .get_info(&bob)
.await
.unwrap()
.contains("KEYCHANGES NOT ALLOWED"));
diff --git a/src/blob.rs b/src/blob.rs
index c7a3641d2..40072cb53 100644
--- a/src/blob.rs
+++ b/src/blob.rs
@@ -9,21 +9,17 @@ use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use futures::StreamExt;
-use image::{DynamicImage, ImageFormat};
+use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
use tokio_stream::wrappers::ReadDirStream;
use crate::config::Config;
-use crate::constants::{
- MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE,
-};
+use crate::constants::{self, MediaQuality};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
-use crate::message;
-use crate::message::Viewtype;
/// Represents a file in the blob directory.
///
@@ -323,119 +319,188 @@ impl<'a> BlobObject<'a> {
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
- MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
- MediaQuality::Worse => WORSE_AVATAR_SIZE,
+ MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE,
+ MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
};
+ let maybe_sticker = &mut false;
+ let strict_limits = true;
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
- if let Some(new_name) = self.recode_to_size(context, blob_abs, img_wh, Some(20_000))? {
+ if let Some(new_name) = self.recode_to_size(
+ context,
+ blob_abs,
+ maybe_sticker,
+ img_wh,
+ 20_000,
+ strict_limits,
+ )? {
self.name = new_name;
}
Ok(())
}
- pub async fn recode_to_image_size(&self, context: &Context) -> Result<()> {
+ /// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
+ /// height and file size specified by the config.
+ ///
+ /// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
+ /// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
+ /// assuming that it must have at least one fully transparent corner, otherwise this flag is
+ /// reset.
+ pub async fn recode_to_image_size(
+ &mut self,
+ context: &Context,
+ maybe_sticker: &mut bool,
+ ) -> Result<()> {
let blob_abs = self.to_abs_path();
- if message::guess_msgtype_from_suffix(Path::new(&blob_abs))
- != Some((Viewtype::Image, "image/jpeg"))
- {
- return Ok(());
- }
-
- let img_wh =
+ let (img_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
- MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
- MediaQuality::Worse => WORSE_IMAGE_SIZE,
+ MediaQuality::Balanced => (
+ constants::BALANCED_IMAGE_SIZE,
+ constants::BALANCED_IMAGE_BYTES,
+ ),
+ MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
};
-
- if self
- .recode_to_size(context, blob_abs, img_wh, None)?
- .is_some()
- {
- return Err(format_err!(
- "Internal error: recode_to_size(..., None) shouldn't change the name of the image"
- ));
+ let strict_limits = false;
+ if let Some(new_name) = self.recode_to_size(
+ context,
+ blob_abs,
+ maybe_sticker,
+ img_wh,
+ max_bytes,
+ strict_limits,
+ )? {
+ self.name = new_name;
}
Ok(())
}
+ /// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
+ /// proceed with the result.
fn recode_to_size(
- &self,
+ &mut self,
context: &Context,
mut blob_abs: PathBuf,
+ maybe_sticker: &mut bool,
mut img_wh: u32,
- max_bytes: Option,
+ max_bytes: usize,
+ strict_limits: bool,
) -> Result> {
- tokio::task::block_in_place(move || {
- let mut img = image::open(&blob_abs).context("image recode failure")?;
- let orientation = self.get_exif_orientation(context);
+ let mut no_exif = false;
+ let no_exif_ref = &mut no_exif;
+ let res = tokio::task::block_in_place(move || {
+ let (nr_bytes, exif) = self.metadata()?;
+ *no_exif_ref = exif.is_none();
+ let mut img = image::open(&blob_abs).context("image decode failure")?;
+ let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
let mut changed_name = None;
- let exceeds_width = img.width() > img_wh || img.height() > img_wh;
+ if *maybe_sticker {
+ let x_max = img.width().saturating_sub(1);
+ let y_max = img.height().saturating_sub(1);
+ *maybe_sticker = img.in_bounds(x_max, y_max)
+ && (img.get_pixel(0, 0).0[3] == 0
+ || img.get_pixel(x_max, 0).0[3] == 0
+ || img.get_pixel(0, y_max).0[3] == 0
+ || img.get_pixel(x_max, y_max).0[3] == 0);
+ }
+ if *maybe_sticker && exif.is_none() {
+ return Ok(None);
+ }
- let do_scale =
- exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
- let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
+ img = match orientation {
+ Some(90) => img.rotate90(),
+ Some(180) => img.rotate180(),
+ Some(270) => img.rotate270(),
+ _ => img,
+ };
- if do_scale || do_rotate {
- if do_rotate {
- img = match orientation {
- Ok(90) => img.rotate90(),
- Ok(180) => img.rotate180(),
- Ok(270) => img.rotate270(),
- _ => img,
- }
+ let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
+ let exceeds_max_bytes = nr_bytes > max_bytes as u64;
+
+ let fmt = ImageFormat::from_path(&blob_abs);
+ let ofmt = match fmt {
+ Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
+ _ => {
+ let jpeg_quality = 75;
+ ImageOutputFormat::Jpeg(jpeg_quality)
}
-
- if do_scale {
- if !exceeds_width {
- // The image is already smaller than img_wh, but exceeds max_bytes
- // We can directly start with trying to scale down to 2/3 of its current width
- img_wh = max(img.width(), img.height()) * 2 / 3
- }
-
- loop {
- let new_img = img.thumbnail(img_wh, img_wh);
-
- if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
- if img_wh < 20 {
- return Err(format_err!(
- "Failed to scale image to below {}B",
- max_bytes.unwrap_or_default()
- ));
- }
-
- img_wh = img_wh * 2 / 3;
- } else {
- if encoded.is_empty() {
- encode_img(&new_img, &mut encoded)?;
- }
-
- info!(
+ };
+ // We need to rewrite images with Exif to remove metadata such as location,
+ // camera model, etc.
+ //
+ // TODO: Fix lost animation and transparency when recoding using the `image` crate. And
+ // also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
+ // images.
+ let do_scale = exceeds_max_bytes
+ || strict_limits
+ && (exceeds_wh
+ || exif.is_some()
+ && encoded_img_exceeds_bytes(
context,
- "Final scaled-down image size: {}B ({}px).",
- encoded.len(),
- img_wh
- );
- break;
- }
+ &img,
+ ofmt.clone(),
+ max_bytes,
+ &mut encoded,
+ )?);
+
+ if do_scale {
+ if !exceeds_wh {
+ img_wh = max(img.width(), img.height());
+ // PNGs and WebPs may be huge because of animation, which is lost by the `image`
+ // crate when recoding, so don't scale them down.
+ if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
+ img_wh = img_wh * 2 / 3;
}
}
- // The file format is JPEG now, we may have to change the file extension
- if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
+ loop {
+ let new_img = img.thumbnail(img_wh, img_wh);
+
+ if encoded_img_exceeds_bytes(
+ context,
+ &new_img,
+ ofmt.clone(),
+ max_bytes,
+ &mut encoded,
+ )? && strict_limits
+ {
+ if img_wh < 20 {
+ return Err(format_err!(
+ "Failed to scale image to below {}B.",
+ max_bytes,
+ ));
+ }
+
+ img_wh = img_wh * 2 / 3;
+ } else {
+ info!(
+ context,
+ "Final scaled-down image size: {}B ({}px).",
+ encoded.len(),
+ img_wh
+ );
+ break;
+ }
+ }
+ }
+
+ if do_scale || exif.is_some() {
+ // The file format is JPEG/PNG now, we may have to change the file extension
+ if !matches!(fmt, Ok(ImageFormat::Jpeg))
+ && matches!(ofmt, ImageOutputFormat::Jpeg(_))
+ {
blob_abs = blob_abs.with_extension("jpg");
- let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
+ let file_name = blob_abs.file_name().context("No image file name (???)")?;
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
changed_name = Some(format!("$BLOBDIR/{file_name}"));
}
if encoded.is_empty() {
- encode_img(&img, &mut encoded)?;
+ encode_img(&img, ofmt, &mut encoded)?;
}
std::fs::write(&blob_abs, &encoded)
@@ -443,26 +508,45 @@ impl<'a> BlobObject<'a> {
}
Ok(changed_name)
- })
- }
-
- pub fn get_exif_orientation(&self, context: &Context) -> Result {
- let file = std::fs::File::open(self.to_abs_path())?;
- let mut bufreader = std::io::BufReader::new(&file);
- let exifreader = exif::Reader::new();
- let exif = exifreader.read_from_container(&mut bufreader)?;
- if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
- // possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
- // we only use rotation, in practise, flipping is not used.
- match orientation.value.get_uint(0) {
- Some(3) => return Ok(180),
- Some(6) => return Ok(90),
- Some(8) => return Ok(270),
- other => warn!(context, "Exif orientation value ignored: {other:?}."),
+ });
+ match res {
+ Ok(_) => res,
+ Err(err) => {
+ if !strict_limits && no_exif {
+ warn!(
+ context,
+ "Cannot recode image, using original data: {err:#}.",
+ );
+ Ok(None)
+ } else {
+ Err(err)
+ }
}
}
- Ok(0)
}
+
+ /// Returns image file size and Exif.
+ pub fn metadata(&self) -> Result<(u64, Option)> {
+ let file = std::fs::File::open(self.to_abs_path())?;
+ let len = file.metadata()?.len();
+ let mut bufreader = std::io::BufReader::new(&file);
+ let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
+ Ok((len, exif))
+ }
+}
+
+fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
+ if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
+ // possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
+ // we only use rotation, in practise, flipping is not used.
+ match orientation.value.get_uint(0) {
+ Some(3) => return 180,
+ Some(6) => return 90,
+ Some(8) => return 270,
+ other => warn!(context, "Exif orientation value ignored: {other:?}."),
+ }
+ }
+ 0
}
impl<'a> fmt::Display for BlobObject<'a> {
@@ -552,31 +636,35 @@ impl<'a> Iterator for BlobDirIter<'a> {
impl FusedIterator for BlobDirIter<'_> {}
-fn encode_img(img: &DynamicImage, encoded: &mut Vec) -> anyhow::Result<()> {
+fn encode_img(
+ img: &DynamicImage,
+ fmt: ImageOutputFormat,
+ encoded: &mut Vec,
+) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
- img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
+ img.write_to(&mut buf, fmt)?;
Ok(())
}
+
fn encoded_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
- max_bytes: Option,
+ fmt: ImageOutputFormat,
+ max_bytes: usize,
encoded: &mut Vec,
) -> anyhow::Result {
- if let Some(max_bytes) = max_bytes {
- encode_img(img, encoded)?;
- if encoded.len() > max_bytes {
- info!(
- context,
- "Image size {}B ({}x{}px) exceeds {}B, need to scale down.",
- encoded.len(),
- img.width(),
- img.height(),
- max_bytes,
- );
- return Ok(true);
- }
+ encode_img(img, fmt, encoded)?;
+ if encoded.len() > max_bytes {
+ info!(
+ context,
+ "Image size {}B ({}x{}px) exceeds {}B, need to scale down.",
+ encoded.len(),
+ img.width(),
+ img.height(),
+ max_bytes,
+ );
+ return Ok(true);
}
Ok(false)
}
@@ -589,7 +677,7 @@ mod tests {
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
- use crate::message::Message;
+ use crate::message::{Message, Viewtype};
use crate::test_utils::{self, TestContext};
fn check_image_size(path: impl AsRef, width: u32, height: u32) -> image::DynamicImage {
@@ -814,17 +902,29 @@ mod tests {
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
check_image_size(avatar_src, 1000, 1000);
- check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
+ check_image_size(
+ &avatar_blob,
+ constants::BALANCED_AVATAR_SIZE,
+ constants::BALANCED_AVATAR_SIZE,
+ );
async fn file_size(path_buf: &Path) -> u64 {
let file = File::open(path_buf).await.unwrap();
file.metadata().await.unwrap().len()
}
- let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
-
- blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
- .unwrap();
+ let mut blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
+ let maybe_sticker = &mut false;
+ let strict_limits = true;
+ blob.recode_to_size(
+ &t,
+ blob.to_abs_path(),
+ maybe_sticker,
+ 1000,
+ 3000,
+ strict_limits,
+ )
+ .unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
tokio::task::block_in_place(move || {
@@ -850,10 +950,14 @@ mod tests {
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(
avatar_cfg,
- avatar_src.with_extension("jpg").to_str().unwrap()
+ avatar_src.with_extension("png").to_str().unwrap()
);
- check_image_size(avatar_cfg, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
+ check_image_size(
+ avatar_cfg,
+ constants::BALANCED_AVATAR_SIZE,
+ constants::BALANCED_AVATAR_SIZE,
+ );
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -879,18 +983,31 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_1() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
- // BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
- send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
- .await
- .unwrap();
send_image_check_mediaquality(
- Some("1"),
+ Viewtype::Image,
+ Some("0"),
bytes,
+ "jpg",
+ true, // has Exif
1000,
1000,
0,
- WORSE_IMAGE_SIZE,
- WORSE_IMAGE_SIZE,
+ 1000,
+ 1000,
+ )
+ .await
+ .unwrap();
+ send_image_check_mediaquality(
+ Viewtype::Image,
+ Some("1"),
+ bytes,
+ "jpg",
+ true, // has Exif
+ 1000,
+ 1000,
+ 0,
+ 1000,
+ 1000,
)
.await
.unwrap();
@@ -901,71 +1018,110 @@ mod tests {
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
let img_rotated = send_image_check_mediaquality(
+ Viewtype::Image,
Some("0"),
bytes,
+ "jpg",
+ true, // has Exif
2000,
1800,
270,
- BALANCED_IMAGE_SIZE * 1800 / 2000,
- BALANCED_IMAGE_SIZE,
+ 1800,
+ 2000,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let mut buf = Cursor::new(vec![]);
- img_rotated
- .write_to(&mut buf, image::ImageFormat::Jpeg)
- .unwrap();
+ img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
let bytes = buf.into_inner();
- // Do this in parallel to speed up the test a bit
- // (it still takes very long though)
- let bytes2 = bytes.clone();
- let join_handle = tokio::task::spawn(async move {
- let img_rotated = send_image_check_mediaquality(
- Some("0"),
- &bytes2,
- BALANCED_IMAGE_SIZE * 1800 / 2000,
- BALANCED_IMAGE_SIZE,
- 0,
- BALANCED_IMAGE_SIZE * 1800 / 2000,
- BALANCED_IMAGE_SIZE,
- )
- .await
- .unwrap();
- assert_correct_rotation(&img_rotated);
- });
-
let img_rotated = send_image_check_mediaquality(
+ Viewtype::Image,
Some("1"),
&bytes,
- BALANCED_IMAGE_SIZE * 1800 / 2000,
- BALANCED_IMAGE_SIZE,
+ "jpg",
+ false, // no Exif
+ 1800,
+ 2000,
0,
- WORSE_IMAGE_SIZE * 1800 / 2000,
- WORSE_IMAGE_SIZE,
+ 1800,
+ 2000,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
-
- join_handle.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_recode_image_3() {
- let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
- let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
- .await
- .unwrap();
- assert_correct_rotation(&img_rotated);
+ async fn test_recode_image_balanced_png() {
+ let bytes = include_bytes!("../test-data/image/screenshot.png");
- let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
- let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
- .await
- .unwrap();
- assert_correct_rotation(&img_rotated);
+ send_image_check_mediaquality(
+ Viewtype::Image,
+ Some("0"),
+ bytes,
+ "png",
+ false, // no Exif
+ 1920,
+ 1080,
+ 0,
+ 1920,
+ 1080,
+ )
+ .await
+ .unwrap();
+
+ send_image_check_mediaquality(
+ Viewtype::Image,
+ Some("1"),
+ bytes,
+ "png",
+ false, // no Exif
+ 1920,
+ 1080,
+ 0,
+ constants::WORSE_IMAGE_SIZE,
+ constants::WORSE_IMAGE_SIZE * 1080 / 1920,
+ )
+ .await
+ .unwrap();
+
+ // This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
+ send_image_check_mediaquality(
+ Viewtype::Sticker,
+ Some("0"),
+ bytes,
+ "png",
+ false, // no Exif
+ 1920,
+ 1080,
+ 0,
+ 1920,
+ 1080,
+ )
+ .await
+ .unwrap();
+ }
+
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_recode_image_huge_jpg() {
+ let bytes = include_bytes!("../test-data/image/screenshot.jpg");
+ send_image_check_mediaquality(
+ Viewtype::Image,
+ Some("0"),
+ bytes,
+ "jpg",
+ true, // has Exif
+ 1920,
+ 1080,
+ 0,
+ constants::BALANCED_IMAGE_SIZE,
+ constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
+ )
+ .await
+ .unwrap();
}
fn assert_correct_rotation(img: &DynamicImage) {
@@ -985,9 +1141,13 @@ mod tests {
assert_eq!(luma, 0);
}
+ #[allow(clippy::too_many_arguments)]
async fn send_image_check_mediaquality(
+ viewtype: Viewtype,
media_quality_config: Option<&str>,
bytes: &[u8],
+ extension: &str,
+ has_exif: bool,
original_width: u32,
original_height: u32,
orientation: i32,
@@ -999,7 +1159,7 @@ mod tests {
alice
.set_config(Config::MediaQuality, media_quality_config)
.await?;
- let file = alice.get_blobdir().join("file.jpg");
+ let file = alice.get_blobdir().join("file").with_extension(extension);
fs::write(&file, &bytes)
.await
@@ -1007,9 +1167,15 @@ mod tests {
check_image_size(&file, original_width, original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
- assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
+ let (_, exif) = blob.metadata()?;
+ if has_exif {
+ let exif = exif.unwrap();
+ assert_eq!(exif_orientation(&exif, &alice), orientation);
+ } else {
+ assert!(exif.is_none());
+ }
- let mut msg = Message::new(Viewtype::Image);
+ let mut msg = Message::new(viewtype);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
@@ -1023,12 +1189,14 @@ mod tests {
);
let bob_msg = bob.recv_msg(&sent).await;
+ assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file = bob_msg.get_file(&bob).unwrap();
let blob = BlobObject::new_from_path(&bob, &file).await?;
- assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
+ let (_, exif) = blob.metadata()?;
+ assert!(exif.is_none());
let img = check_image_size(file, compressed_width, compressed_height);
Ok(img)
diff --git a/src/chat.rs b/src/chat.rs
index 7561c511f..b6d593822 100644
--- a/src/chat.rs
+++ b/src/chat.rs
@@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
+use crate::chatlist::Chatlist;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
@@ -22,9 +23,11 @@ use crate::constants::{
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
+use crate::download::DownloadState;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
+use crate::location;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -33,6 +36,7 @@ use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::receive_imf::ReceivedMsg;
use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
+use crate::sql;
use crate::stock_str;
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
@@ -40,7 +44,6 @@ use crate::tools::{
strip_rtlo_characters, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
-use crate::{location, sql};
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -86,6 +89,14 @@ pub enum ProtectionStatus {
///
/// All members of the chat must be verified.
Protected = 1,
+
+ /// The chat was protected, but now a new message came in
+ /// which was not encrypted / signed correctly.
+ /// The user has to confirm that this is OK.
+ ///
+ /// We only do this in 1:1 chats; in group chats, the chat just
+ /// stays protected.
+ ProtectionBroken = 3, // `2` was never used as a value.
}
/// The reason why messages cannot be sent to the chat.
@@ -102,6 +113,10 @@ pub(crate) enum CantSendReason {
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
+ /// The chat was protected, but now a new message came in
+ /// which was not encrypted / signed correctly.
+ ProtectionBroken,
+
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
@@ -118,6 +133,10 @@ impl fmt::Display for CantSendReason {
f,
"contact request chat should be accepted before sending messages"
),
+ Self::ProtectionBroken => write!(
+ f,
+ "accept that the encryption isn't verified anymore before sending messages"
+ ),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
@@ -270,6 +289,7 @@ impl ChatId {
param: Option,
) -> Result {
let grpname = strip_rtlo_characters(grpname);
+ let smeared_time = create_smeared_timestamp(context);
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
@@ -278,13 +298,20 @@ impl ChatId {
&grpname,
grpid,
create_blocked,
- create_smeared_timestamp(context),
+ smeared_time,
create_protected,
param.unwrap_or_default(),
),
).await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
+
+ if create_protected == ProtectionStatus::Protected {
+ chat_id
+ .add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time)
+ .await?;
+ }
+
info!(
context,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
@@ -332,7 +359,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
- Chattype::Undefined | Chattype::Broadcast => {
+ Chattype::Broadcast => {
bail!("Can't block chat of type {:?}", chat.typ)
}
Chattype::Single => {
@@ -373,7 +400,16 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
- Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
+ Chattype::Single
+ if chat.blocked == Blocked::Not
+ && chat.protected == ProtectionStatus::ProtectionBroken =>
+ {
+ // The protection was broken, then the user clicked 'Accept'/'OK',
+ // so, now we want to set the status to Unprotected again:
+ chat.id
+ .inner_set_protection(context, ProtectionStatus::Unprotected)
+ .await?;
+ }
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
// User has "created a chat" with all these contacts.
//
@@ -400,20 +436,19 @@ impl ChatId {
/// Sets protection without sending a message.
///
- /// Used when a message arrives indicating that someone else has
- /// changed the protection value for a chat.
+ /// Returns whether the protection status was actually modified.
pub(crate) async fn inner_set_protection(
self,
context: &Context,
protect: ProtectionStatus,
- ) -> Result<()> {
- ensure!(!self.is_special(), "Invalid chat-id.");
+ ) -> Result {
+ ensure!(!self.is_special(), "Invalid chat-id {self}.");
let chat = Chat::load_from_db(context, self).await?;
if protect == chat.protected {
info!(context, "Protection status unchanged for {}.", self);
- return Ok(());
+ return Ok(false);
}
match protect {
@@ -428,9 +463,8 @@ impl ChatId {
}
}
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
- Chattype::Undefined => bail!("Undefined group type"),
},
- ProtectionStatus::Unprotected => {}
+ ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
};
context
@@ -443,68 +477,58 @@ impl ChatId {
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
- Ok(())
+ Ok(true)
}
- /// Send protected status message to the chat.
+ /// Adds an info message to the chat, telling the user that the protection status changed.
///
- /// This sends the message with the protected status change to the chat,
- /// notifying the user on this device as well as the other users in the chat.
+ /// Params:
///
- /// If `promote` is false this means, the message must not be sent out
- /// and only a local info message should be added to the chat.
- /// This is used when protection is enabled implicitly or when a chat is not yet promoted.
+ /// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id.
+ /// * `timestamp_sort` is used as the timestamp of the added message
+ /// and should be the timestamp of the change happening.
pub(crate) async fn add_protection_msg(
self,
context: &Context,
protect: ProtectionStatus,
- promote: bool,
- from_id: ContactId,
+ contact_id: Option,
+ timestamp_sort: i64,
) -> Result<()> {
- let msg_text = context.stock_protection_msg(protect, from_id).await;
+ let text = context.stock_protection_msg(protect, contact_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
+ ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
-
- if promote {
- let mut msg = Message {
- viewtype: Viewtype::Text,
- text: Some(msg_text),
- ..Default::default()
- };
- msg.param.set_cmd(cmd);
- send_msg(context, self, &mut msg).await?;
- } else {
- add_info_msg_with_cmd(
- context,
- self,
- &msg_text,
- cmd,
- create_smeared_timestamp(context),
- None,
- None,
- None,
- )
- .await?;
- }
+ add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
Ok(())
}
/// Sets protection and sends or adds a message.
- pub async fn set_protection(self, context: &Context, protect: ProtectionStatus) -> Result<()> {
- ensure!(!self.is_special(), "set protection: invalid chat-id.");
-
- let chat = Chat::load_from_db(context, self).await?;
-
- if let Err(e) = self.inner_set_protection(context, protect).await {
- error!(context, "Cannot set protection: {e:#}."); // make error user-visible
- return Err(e);
+ ///
+ /// `timestamp_sort` is used as the timestamp of the added message
+ /// and should be the timestamp of the change happening.
+ pub(crate) async fn set_protection(
+ self,
+ context: &Context,
+ protect: ProtectionStatus,
+ timestamp_sort: i64,
+ contact_id: Option,
+ ) -> Result<()> {
+ match self.inner_set_protection(context, protect).await {
+ Ok(protection_status_modified) => {
+ if protection_status_modified {
+ self.add_protection_msg(context, protect, contact_id, timestamp_sort)
+ .await?;
+ }
+ Ok(())
+ }
+ Err(e) => {
+ error!(context, "Cannot set protection: {e:#}."); // make error user-visible
+ Err(e)
+ }
}
-
- self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF)
- .await
}
/// Archives or unarchives a chat.
@@ -642,7 +666,7 @@ impl ChatId {
if chat.is_self_talk() {
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some(stock_str::self_deleted_msg_body(context).await);
+ msg.text = stock_str::self_deleted_msg_body(context).await;
add_device_msg(context, None, Some(&mut msg)).await?;
}
@@ -724,7 +748,7 @@ impl ChatId {
match msg.viewtype {
Viewtype::Unknown => bail!("Can not set draft of unknown type."),
Viewtype::Text => {
- if msg.text.is_none_or_empty() && msg.in_reply_to.is_none_or_empty() {
+ if msg.text.is_empty() && msg.in_reply_to.is_none_or_empty() {
bail!("No text and no quote in draft");
}
}
@@ -741,14 +765,6 @@ impl ChatId {
}
}
- let chat = Chat::load_from_db(context, self).await?;
- if let Some(cant_send_reason) = chat.why_cant_send(context).await? {
- bail!(
- "Can't set a draft because chat is not writeable: {}",
- cant_send_reason
- );
- }
-
// set back draft information to allow identifying the draft later on -
// no matter if message object is reused or reloaded from db
msg.state = MessageState::OutDraft;
@@ -770,7 +786,7 @@ impl ChatId {
(
time(),
msg.viewtype,
- msg.text.as_deref().unwrap_or(""),
+ &msg.text,
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
@@ -804,7 +820,7 @@ impl ChatId {
time(),
msg.viewtype,
MessageState::OutDraft,
- msg.text.as_deref().unwrap_or(""),
+ &msg.text,
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
@@ -871,6 +887,134 @@ impl ChatId {
Ok(count)
}
+ /// Returns timestamp of the latest message in the chat.
+ pub(crate) async fn get_timestamp(self, context: &Context) -> Result> {
+ let timestamp = context
+ .sql
+ .query_get_value("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", (self,))
+ .await?;
+ Ok(timestamp)
+ }
+
+ /// Returns a list of active similar chat IDs sorted by similarity metric.
+ ///
+ /// Jaccard similarity coefficient is used to estimate similarity of chat member sets.
+ ///
+ /// Chat is considered active if something was posted there within the last 42 days.
+ pub async fn get_similar_chat_ids(self, context: &Context) -> Result> {
+ // Count number of common members in this and other chats.
+ let intersection: Vec<(ChatId, f64)> = context
+ .sql
+ .query_map(
+ "SELECT y.chat_id, SUM(x.contact_id = y.contact_id)
+ FROM chats_contacts as x
+ JOIN chats_contacts as y
+ WHERE x.contact_id > 9
+ AND y.contact_id > 9
+ AND x.chat_id=?
+ AND y.chat_id<>x.chat_id
+ GROUP BY y.chat_id",
+ (self,),
+ |row| {
+ let chat_id: ChatId = row.get(0)?;
+ let intersection: f64 = row.get(1)?;
+ Ok((chat_id, intersection))
+ },
+ |rows| {
+ rows.collect::, _>>()
+ .map_err(Into::into)
+ },
+ )
+ .await
+ .context("failed to calculate member set intersections")?;
+
+ let chat_size: HashMap = context
+ .sql
+ .query_map(
+ "SELECT chat_id, count(*) AS n
+ FROM chats_contacts
+ WHERE contact_id > ? AND chat_id > ?
+ GROUP BY chat_id",
+ (ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL),
+ |row| {
+ let chat_id: ChatId = row.get(0)?;
+ let size: f64 = row.get(1)?;
+ Ok((chat_id, size))
+ },
+ |rows| {
+ rows.collect::, _>>()
+ .map_err(Into::into)
+ },
+ )
+ .await
+ .context("failed to count chat member sizes")?;
+
+ let our_chat_size = chat_size.get(&self).copied().unwrap_or_default();
+ let mut chats_with_metrics = Vec::new();
+ for (chat_id, intersection_size) in intersection {
+ if intersection_size > 0.0 {
+ let other_chat_size = chat_size.get(&chat_id).copied().unwrap_or_default();
+ let union_size = our_chat_size + other_chat_size - intersection_size;
+ let metric = intersection_size / union_size;
+ chats_with_metrics.push((chat_id, metric))
+ }
+ }
+ chats_with_metrics.sort_unstable_by(|(chat_id1, metric1), (chat_id2, metric2)| {
+ metric2
+ .partial_cmp(metric1)
+ .unwrap_or(chat_id2.cmp(chat_id1))
+ });
+
+ // Select up to five similar active chats.
+ let mut res = Vec::new();
+ let now = time();
+ for (chat_id, metric) in chats_with_metrics {
+ if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
+ if now > chat_timestamp + 42 * 24 * 3600 {
+ // Chat was inactive for 42 days, skip.
+ continue;
+ }
+ }
+
+ if metric < 0.1 {
+ // Chat is unrelated.
+ break;
+ }
+
+ let chat = Chat::load_from_db(context, chat_id).await?;
+ if chat.typ != Chattype::Group {
+ continue;
+ }
+
+ match chat.visibility {
+ ChatVisibility::Normal | ChatVisibility::Pinned => {}
+ ChatVisibility::Archived => continue,
+ }
+
+ res.push((chat_id, metric));
+ if res.len() >= 5 {
+ break;
+ }
+ }
+
+ Ok(res)
+ }
+
+ /// Returns similar chats as a [`Chatlist`].
+ ///
+ /// [`Chatlist`]: crate::chatlist::Chatlist
+ pub async fn get_similar_chatlist(self, context: &Context) -> Result {
+ let chat_ids: Vec = self
+ .get_similar_chat_ids(context)
+ .await
+ .context("failed to get similar chat IDs")?
+ .into_iter()
+ .map(|(chat_id, _metric)| chat_id)
+ .collect();
+ let chatlist = Chatlist::from_chat_ids(context, &chat_ids).await?;
+ Ok(chatlist)
+ }
+
pub(crate) async fn get_param(self, context: &Context) -> Result {
let res: Option = context
.sql
@@ -910,11 +1054,14 @@ impl ChatId {
T: Send + 'static,
{
let sql = &context.sql;
+ // Do not reply to not fully downloaded messages. Such a message could be a group chat
+ // message that we assigned to 1:1 chat.
let query = format!(
"SELECT {fields} \
- FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \
+ FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden AND download_state={} \
ORDER BY timestamp DESC, id DESC \
- LIMIT 1;"
+ LIMIT 1;",
+ DownloadState::Done as u32,
);
let row = sql
.query_row_optional(
@@ -923,8 +1070,11 @@ impl ChatId {
self,
MessageState::OutPreparing,
MessageState::OutDraft,
- MessageState::OutPending,
- MessageState::OutFailed,
+ // We don't filter `OutPending` and `OutFailed` messages because the new message
+ // for which `parent_query()` is done may assume that it will be received in a
+ // context affected by those messages, e.g. they could add new members to a
+ // group and the new message will contain them in "To:". Anyway recipients must
+ // be prepared to orphaned references.
),
f,
)
@@ -936,34 +1086,17 @@ impl ChatId {
self,
context: &Context,
) -> Result> {
- if let Some((rfc724_mid, mime_in_reply_to, mime_references, error)) = self
- .parent_query(
- context,
- "rfc724_mid, mime_in_reply_to, mime_references, error",
- |row: &rusqlite::Row| {
- let rfc724_mid: String = row.get(0)?;
- let mime_in_reply_to: String = row.get(1)?;
- let mime_references: String = row.get(2)?;
- let error: String = row.get(3)?;
- Ok((rfc724_mid, mime_in_reply_to, mime_references, error))
- },
- )
- .await?
- {
- if !error.is_empty() {
- // Do not reply to error messages.
- //
- // An error message could be a group chat message that we failed to decrypt and
- // assigned to 1:1 chat. A reply to it will show up as a reply to group message
- // on the other side. To avoid such situations, it is better not to reply to
- // error messages at all.
- Ok(None)
- } else {
- Ok(Some((rfc724_mid, mime_in_reply_to, mime_references)))
- }
- } else {
- Ok(None)
- }
+ self.parent_query(
+ context,
+ "rfc724_mid, mime_in_reply_to, mime_references",
+ |row: &rusqlite::Row| {
+ let rfc724_mid: String = row.get(0)?;
+ let mime_in_reply_to: String = row.get(1)?;
+ let mime_references: String = row.get(2)?;
+ Ok((rfc724_mid, mime_in_reply_to, mime_references))
+ },
+ )
+ .await
}
/// Returns multi-line text summary of encryption preferences of all chat contacts.
@@ -983,7 +1116,7 @@ impl ChatId {
.iter()
.filter(|&contact_id| !contact_id.is_special())
{
- let contact = Contact::load_from_db(context, *contact_id).await?;
+ let contact = Contact::get_by_id(context, *contact_id).await?;
let addr = contact.get_addr();
let peerstate = Peerstate::from_addr(context, addr).await?;
@@ -1141,7 +1274,7 @@ pub struct Chat {
pub grpid: String,
/// Whether the chat is blocked, unblocked or a contact request.
- pub(crate) blocked: Blocked,
+ pub blocked: Blocked,
/// Additional chat parameters stored in the database.
pub param: Params,
@@ -1153,7 +1286,7 @@ pub struct Chat {
pub mute_duration: MuteDuration,
/// If the chat is protected (verified).
- protected: ProtectionStatus,
+ pub(crate) protected: ProtectionStatus,
}
impl Chat {
@@ -1241,13 +1374,16 @@ impl Chat {
pub(crate) async fn why_cant_send(&self, context: &Context) -> Result > {
use CantSendReason::*;
+ // NB: Don't forget to update Chatlist::try_load() when changing this function!
let reason = if self.id.is_special() {
Some(SpecialChat)
} else if self.is_device_talk() {
Some(DeviceChat)
} else if self.is_contact_request() {
Some(ContactRequest)
- } else if self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty() {
+ } else if self.is_protection_broken() {
+ Some(ProtectionBroken)
+ } else if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
Some(NotAMember)
@@ -1271,7 +1407,6 @@ impl Chat {
match self.typ {
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
- Chattype::Undefined => Ok(false),
}
}
@@ -1310,11 +1445,11 @@ impl Chat {
pub async fn get_profile_image(&self, context: &Context) -> Result > {
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
- return Ok(Some(get_abs_path(context, image_rel)));
+ return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
}
} else if self.id.is_archived_link() {
if let Ok(image_rel) = get_archive_icon(context).await {
- return Ok(Some(get_abs_path(context, image_rel)));
+ return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
}
} else if self.typ == Chattype::Single {
let contacts = get_chat_contacts(context, self.id).await?;
@@ -1325,7 +1460,7 @@ impl Chat {
}
} else if self.typ == Chattype::Broadcast {
if let Ok(image_rel) = get_broadcast_icon(context).await {
- return Ok(Some(get_abs_path(context, image_rel)));
+ return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
}
}
Ok(None)
@@ -1358,7 +1493,7 @@ impl Chat {
/// deltachat, and the data returned is still subject to change.
pub async fn get_info(&self, context: &Context) -> Result {
let draft = match self.id.get_draft(context).await? {
- Some(message) => message.text.unwrap_or_default(),
+ Some(message) => message.text,
_ => String::new(),
};
Ok(ChatInfo {
@@ -1400,6 +1535,7 @@ impl Chat {
}
/// Returns true if the chat is promoted.
+ /// This means a message has been sent to it and it _not_ only exists on the users device.
pub fn is_promoted(&self) -> bool {
!self.is_unpromoted()
}
@@ -1409,6 +1545,27 @@ impl Chat {
self.protected == ProtectionStatus::Protected
}
+ /// Returns true if the chat was protected, and then an incoming message broke this protection.
+ ///
+ /// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
+ /// otherwise it will return false for all chats.
+ ///
+ /// 1:1 chats are automatically set as protected when a contact is verified.
+ /// When a message comes in that is not encrypted / signed correctly,
+ /// the chat is automatically set as unprotected again.
+ /// `is_protection_broken()` will return true until `chat_id.accept()` is called.
+ ///
+ /// The UI should let the user confirm that this is OK with a message like
+ /// `Bob sent a message from another device. Tap to learn more`
+ /// and then call `chat_id.accept()`.
+ pub fn is_protection_broken(&self) -> bool {
+ match self.protected {
+ ProtectionStatus::Protected => false,
+ ProtectionStatus::Unprotected => false,
+ ProtectionStatus::ProtectionBroken => true,
+ }
+ }
+
/// Returns true if location streaming is enabled in the chat.
pub fn is_sending_locations(&self) -> bool {
self.is_sending_locations
@@ -1439,15 +1596,6 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
- if let Some(reason) = self.why_cant_send(context).await? {
- if self.typ == Chattype::Group && reason == CantSendReason::NotAMember {
- context.emit_event(EventType::ErrorSelfNotInGroup(
- "Cannot send message; self not in group.".into(),
- ));
- }
- bail!("Cannot send message to {}: {}", self.id, reason);
- }
-
let from = context.get_primary_self_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
@@ -1583,6 +1731,11 @@ impl Chat {
None
};
+ msg.chat_id = self.id;
+ msg.from_id = ContactId::SELF;
+ msg.rfc724_mid = new_rfc724_mid;
+ msg.timestamp_sort = timestamp;
+
// add message to the database
if let Some(update_msg_id) = update_msg_id {
context
@@ -1596,14 +1749,14 @@ impl Chat {
ephemeral_timestamp=?
WHERE id=?;",
params_slice![
- new_rfc724_mid,
- self.id,
- ContactId::SELF,
+ msg.rfc724_mid,
+ msg.chat_id,
+ msg.from_id,
to_id,
- timestamp,
+ msg.timestamp_sort,
msg.viewtype,
msg.state,
- msg.text.as_ref().cloned().unwrap_or_default(),
+ msg.text,
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -1645,14 +1798,14 @@ impl Chat {
ephemeral_timestamp)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
params_slice![
- new_rfc724_mid,
- self.id,
- ContactId::SELF,
+ msg.rfc724_mid,
+ msg.chat_id,
+ msg.from_id,
to_id,
- timestamp,
+ msg.timestamp_sort,
msg.viewtype,
msg.state,
- msg.text.as_ref().cloned().unwrap_or_default(),
+ msg.text,
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -1808,7 +1961,7 @@ pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
chat.param.set(Param::ProfileImage, &icon);
chat.update_param(context).await?;
- let mut contact = Contact::load_from_db(context, ContactId::DEVICE).await?;
+ let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?;
contact.param.set(Param::ProfileImage, icon);
contact.update_param(context).await?;
}
@@ -1932,7 +2085,7 @@ impl ChatIdBlocked {
/// Returns the chat for the 1:1 chat with this contact.
///
- /// I the chat does not yet exist a new one is created, using the provided [`Blocked`]
+ /// If the chat does not yet exist a new one is created, using the provided [`Blocked`]
/// state.
pub async fn get_for_contact(
context: &Context,
@@ -1950,7 +2103,7 @@ impl ChatIdBlocked {
return Ok(res);
}
- let contact = Contact::load_from_db(context, contact_id).await?;
+ let contact = Contact::get_by_id(context, contact_id).await?;
let chat_name = contact.get_display_name().to_string();
let mut params = Params::new();
match contact_id {
@@ -1963,19 +2116,30 @@ impl ChatIdBlocked {
_ => (),
}
+ 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
.sql
.transaction(move |transaction| {
transaction.execute(
"INSERT INTO chats
- (type, name, param, blocked, created_timestamp)
- VALUES(?, ?, ?, ?, ?)",
+ (type, name, param, blocked, created_timestamp, protected)
+ VALUES(?, ?, ?, ?, ?, ?)",
(
Chattype::Single,
chat_name,
params.to_string(),
create_blocked as u8,
- create_smeared_timestamp(context),
+ smeared_time,
+ if protected {
+ ProtectionStatus::Protected
+ } else {
+ ProtectionStatus::Unprotected
+ },
),
)?;
let chat_id = ChatId::new(
@@ -1996,6 +2160,17 @@ impl ChatIdBlocked {
})
.await?;
+ if protected {
+ chat_id
+ .add_protection_msg(
+ context,
+ ProtectionStatus::Protected,
+ Some(contact_id),
+ smeared_time,
+ )
+ .await?;
+ }
+
match contact_id {
ContactId::SELF => update_saved_messages_icon(context).await?,
ContactId::DEVICE => update_device_icon(context).await?,
@@ -2026,21 +2201,32 @@ 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 blob = msg
+ let mut blob = msg
.param
.get_blob(Param::File, context, !msg.is_increation())
.await?
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
- if msg.viewtype == Viewtype::Image {
- if let Err(err) = blob.recode_to_image_size(context).await {
- warn!(
- context,
- "Cannot recode image, using original data: {err:#}."
- );
+ let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
+ if msg.viewtype == Viewtype::Image
+ || maybe_sticker && !msg.param.exists(Param::ForceSticker)
+ {
+ blob.recode_to_image_size(context, &mut maybe_sticker)
+ .await?;
+
+ if !maybe_sticker {
+ msg.viewtype = Viewtype::Image;
}
}
msg.param.set(Param::File, blob.as_name());
+ if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) {
+ let stem = match filename.rsplit_once('.') {
+ Some((stem, _)) => stem,
+ None => filename,
+ };
+ msg.param
+ .set(Param::Filename, stem.to_string() + "." + blob_ext);
+ }
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
// Correct the type, take care not to correct already very special
@@ -2076,6 +2262,8 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
}
}
+ msg.try_calc_and_set_dimensions(context).await?;
+
info!(
context,
"Attaching \"{}\" for message type #{}.",
@@ -2099,7 +2287,13 @@ async fn prepare_msg_common(
// Check if the chat can be sent to.
if let Some(reason) = chat.why_cant_send(context).await? {
- bail!("cannot send to {}: {}", chat_id, reason);
+ if reason == CantSendReason::ProtectionBroken
+ && msg.param.get_cmd() == SystemMessage::SecurejoinMessage
+ {
+ // Send out the message, the securejoin message is supposed to repair the verification
+ } else {
+ bail!("cannot send to {chat_id}: {reason}");
+ }
}
// check current MessageState for drafts (to keep msg_id) ...
@@ -2201,11 +2395,9 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result {
- // protect all system messages againts RTLO attacks
+ // protect all system messages against RTLO attacks
if msg.is_system_message() {
- if let Some(text) = &msg.text {
- msg.text = Some(strip_rtlo_characters(text.as_ref()));
- }
+ msg.text = strip_rtlo_characters(&msg.text);
}
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
@@ -2245,7 +2437,7 @@ async fn prepare_send_msg(
);
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
}
- let row_id = create_send_msg_job(context, msg.id).await?;
+ let row_id = create_send_msg_job(context, msg).await?;
Ok(row_id)
}
@@ -2255,13 +2447,10 @@ async fn prepare_send_msg(
/// group with only self and no BCC-to-self configured.
///
/// The caller has to interrupt SMTP loop or otherwise process a new row.
-async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result> {
- let mut msg = Message::load_from_db(context, msg_id).await?;
- msg.try_calc_and_set_dimensions(context)
- .await
- .context("failed to calculate media dimensions")?;
-
- /* create message */
+pub(crate) async fn create_send_msg_job(
+ context: &Context,
+ msg: &mut Message,
+) -> Result > {
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
@@ -2272,7 +2461,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result Result Ok(res),
Err(err) => {
- message::set_msg_failed(context, msg_id, &err.to_string()).await;
+ message::set_msg_failed(context, msg, &err.to_string()).await?;
Err(err)
}
}?;
@@ -2312,13 +2502,13 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result Result Result Re
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance);
- msg.text = Some(
+ msg.text =
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
- .await,
- );
+ .await;
send_msg(context, chat_id, &mut msg).await
}
@@ -2568,14 +2756,7 @@ pub(crate) async fn marknoticed_chat_if_older_than(
chat_id: ChatId,
timestamp: i64,
) -> Result<()> {
- if let Some(chat_timestamp) = context
- .sql
- .query_get_value(
- "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
- (chat_id,),
- )
- .await?
- {
+ if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
if timestamp > chat_timestamp {
marknoticed_chat(context, chat_id).await?;
}
@@ -2774,6 +2955,9 @@ pub enum Direction {
}
/// Searches next/previous message based on the given message and list of types.
+///
+/// Deprecated since 2023-10-03.
+#[deprecated(note = "use `get_chat_media` instead")]
pub async fn get_next_media(
context: &Context,
curr_msg_id: MsgId,
@@ -2852,18 +3036,14 @@ pub async fn create_group_chat(
let grpid = create_id();
+ let timestamp = create_smeared_timestamp(context);
let row_id = context
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
- (
- Chattype::Group,
- chat_name,
- grpid,
- create_smeared_timestamp(context),
- ),
+ (Chattype::Group, chat_name, grpid, timestamp),
)
.await?;
@@ -2875,9 +3055,9 @@ pub async fn create_group_chat(
context.emit_msgs_changed_without_ids();
if protect == ProtectionStatus::Protected {
- // this part is to stay compatible to verified groups,
- // in some future, we will drop the "protect"-flag from create_group_chat()
- chat_id.inner_set_protection(context, protect).await?;
+ chat_id
+ .set_protection(context, protect, timestamp, None)
+ .await?;
}
Ok(chat_id)
@@ -2969,6 +3149,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
}
/// Adds a contact to the chat.
+/// If the group is promoted, also sends out a system message to all group members
pub async fn add_contact_to_chat(
context: &Context,
chat_id: ChatId,
@@ -2990,7 +3171,7 @@ pub(crate) async fn add_contact_to_chat_ex(
chat_id.reset_gossiped_timestamp(context).await?;
- /*this also makes sure, not contacts are added to special or normal chats*/
+ // this also makes sure, no contacts are added to special or normal chats
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
@@ -3012,7 +3193,7 @@ pub(crate) async fn add_contact_to_chat_ex(
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot add contact to group; self not in group.".into(),
));
- bail!("can not add contact because our account is not part of it");
+ bail!("can not add contact because the account is not part of the group/broadcast");
}
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
@@ -3055,10 +3236,10 @@ pub(crate) async fn add_contact_to_chat_ex(
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
- msg.text =
- Some(stock_str::msg_add_member(context, contact.get_addr(), ContactId::SELF).await);
+ let contact_addr = contact.get_addr();
+ msg.text = stock_str::msg_add_member_local(context, contact_addr, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
- msg.param.set(Param::Arg, contact.get_addr());
+ msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
msg.id = send_msg(context, chat_id, &mut msg).await?;
}
@@ -3178,59 +3359,53 @@ pub async fn remove_contact_from_chat(
);
let mut msg = Message::default();
- let mut success = false;
- /* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */
- /* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
- if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
- if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
- if !chat.is_self_in_chat(context).await? {
- context.emit_event(EventType::ErrorSelfNotInGroup(
- "Cannot remove contact from chat; self not in group.".into(),
- ));
- } else {
- if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
- if chat.typ == Chattype::Group && chat.is_promoted() {
- msg.viewtype = Viewtype::Text;
- if contact.id == ContactId::SELF {
- set_group_explicitly_left(context, &chat.grpid).await?;
- msg.text =
- Some(stock_str::msg_group_left(context, ContactId::SELF).await);
- } else {
- msg.text = Some(
- stock_str::msg_del_member(
- context,
- contact.get_addr(),
- ContactId::SELF,
- )
- .await,
- );
- }
- msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
- msg.param.set(Param::Arg, contact.get_addr());
- msg.id = send_msg(context, chat_id, &mut msg).await?;
+ let chat = Chat::load_from_db(context, chat_id).await?;
+ if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
+ if !chat.is_self_in_chat(context).await? {
+ let err_msg = format!(
+ "Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
+ );
+ context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
+ bail!("{}", err_msg);
+ } else {
+ // We do not return an error if the contact does not exist in the database.
+ // This allows to delete dangling references to deleted contacts
+ // in case of the database becoming inconsistent due to a bug.
+ if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
+ if chat.typ == Chattype::Group && chat.is_promoted() {
+ msg.viewtype = Viewtype::Text;
+ if contact.id == ContactId::SELF {
+ set_group_explicitly_left(context, &chat.grpid).await?;
+ msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
+ } else {
+ msg.text = stock_str::msg_del_member_local(
+ context,
+ contact.get_addr(),
+ ContactId::SELF,
+ )
+ .await;
}
+ msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
+ msg.param.set(Param::Arg, contact.get_addr());
+ msg.id = send_msg(context, chat_id, &mut msg).await?;
}
- // we remove the member from the chat after constructing the
- // to-be-send message. If between send_msg() and here the
- // process dies the user will have to re-do the action. It's
- // better than the other way round: you removed
- // someone from DB but no peer or device gets to know about it and
- // group membership is thus different on different devices.
- // Note also that sending a message needs all recipients
- // in order to correctly determine encryption so if we
- // removed it first, it would complicate the
- // check/encryption logic.
- success = remove_from_chat_contacts_table(context, chat_id, contact_id)
- .await
- .is_ok();
- context.emit_event(EventType::ChatModified(chat_id));
}
+ // we remove the member from the chat after constructing the
+ // to-be-send message. If between send_msg() and here the
+ // process dies the user will have to re-do the action. It's
+ // better than the other way round: you removed
+ // someone from DB but no peer or device gets to know about it and
+ // group membership is thus different on different devices.
+ // Note also that sending a message needs all recipients
+ // in order to correctly determine encryption so if we
+ // removed it first, it would complicate the
+ // check/encryption logic.
+ remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
+ context.emit_event(EventType::ChatModified(chat_id));
}
- }
-
- if !success {
- bail!("Failed to remove contact");
+ } else {
+ bail!("Cannot remove members from non-group chats.");
}
Ok(())
@@ -3291,9 +3466,8 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
&& improve_single_line_input(&chat.name) != new_name
{
msg.viewtype = Viewtype::Text;
- msg.text = Some(
- stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await,
- );
+ msg.text =
+ stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::GroupNameChanged);
if !chat.name.is_empty() {
msg.param.set(Param::Arg, &chat.name);
@@ -3342,13 +3516,13 @@ pub async fn set_chat_profile_image(
if new_image.is_empty() {
chat.param.remove(Param::ProfileImage);
msg.param.remove(Param::Arg);
- msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
+ msg.text = stock_str::msg_grp_img_deleted(context, ContactId::SELF).await;
} else {
let mut image_blob = BlobObject::new_from_path(context, Path::new(new_image)).await?;
image_blob.recode_to_avatar_size(context).await?;
chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name());
- msg.text = Some(stock_str::msg_grp_img_changed(context, ContactId::SELF).await);
+ msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await;
}
chat.update_param(context).await?;
if chat.is_promoted() && !chat.is_mailing_list() {
@@ -3371,89 +3545,88 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
chat_id
.unarchive_if_not_muted(context, MessageState::Undefined)
.await?;
- if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
- if let Some(reason) = chat.why_cant_send(context).await? {
- bail!("cannot send to {}: {}", chat_id, reason);
+ let mut chat = Chat::load_from_db(context, chat_id).await?;
+ if let Some(reason) = chat.why_cant_send(context).await? {
+ bail!("cannot send to {}: {}", chat_id, reason);
+ }
+ curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
+ let ids = context
+ .sql
+ .query_map(
+ &format!(
+ "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
+ sql::repeat_vars(msg_ids.len())
+ ),
+ rusqlite::params_from_iter(msg_ids),
+ |row| row.get::<_, MsgId>(0),
+ |ids| ids.collect::, _>>().map_err(Into::into),
+ )
+ .await?;
+
+ for id in ids {
+ let src_msg_id: MsgId = id;
+ let mut msg = Message::load_from_db(context, src_msg_id).await?;
+ if msg.state == MessageState::OutDraft {
+ bail!("cannot forward drafts.");
}
- curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
- let ids = context
- .sql
- .query_map(
- &format!(
- "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
- sql::repeat_vars(msg_ids.len())
- ),
- rusqlite::params_from_iter(msg_ids),
- |row| row.get::<_, MsgId>(0),
- |ids| ids.collect::, _>>().map_err(Into::into),
- )
- .await?;
- for id in ids {
- let src_msg_id: MsgId = id;
- let mut msg = Message::load_from_db(context, src_msg_id).await?;
- if msg.state == MessageState::OutDraft {
- bail!("cannot forward drafts.");
- }
+ let original_param = msg.param.clone();
- 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.
- // we tested a sort of broadcast
- // by not marking own forwarded messages as such,
- // however, this turned out to be to confusing and unclear.
+ if msg.get_viewtype() != Viewtype::Sticker {
+ msg.param
+ .set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
+ }
- if msg.get_viewtype() != Viewtype::Sticker {
- msg.param
- .set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
- }
+ msg.param.remove(Param::GuaranteeE2ee);
+ msg.param.remove(Param::ForcePlaintext);
+ msg.param.remove(Param::Cmd);
+ msg.param.remove(Param::OverrideSenderDisplayname);
+ msg.param.remove(Param::WebxdcDocument);
+ msg.param.remove(Param::WebxdcDocumentTimestamp);
+ msg.param.remove(Param::WebxdcSummary);
+ msg.param.remove(Param::WebxdcSummaryTimestamp);
+ msg.in_reply_to = None;
- msg.param.remove(Param::GuaranteeE2ee);
- msg.param.remove(Param::ForcePlaintext);
- msg.param.remove(Param::Cmd);
- msg.param.remove(Param::OverrideSenderDisplayname);
- msg.param.remove(Param::WebxdcSummary);
- msg.param.remove(Param::WebxdcSummaryTimestamp);
- msg.in_reply_to = None;
+ // do not leak data as group names; a default subject is generated by mimefactory
+ msg.subject = "".to_string();
- // do not leak data as group names; a default subject is generated by mimefactory
- msg.subject = "".to_string();
+ 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;
- 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;
- let save_param = msg.param.clone();
- 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?;
- msg.param = save_param;
+ 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.state = MessageState::OutPending;
- new_msg_id = chat
- .prepare_msg_raw(context, &mut msg, None, curr_timestamp)
- .await?;
- curr_timestamp += 1;
- if create_send_msg_job(context, new_msg_id).await?.is_some() {
- context
- .scheduler
- .interrupt_smtp(InterruptInfo::new(false))
- .await;
- }
+ 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_job(context, &mut msg).await?.is_some() {
+ context
+ .scheduler
+ .interrupt_smtp(InterruptInfo::new(false))
+ .await;
}
- created_chats.push(chat_id);
- created_msgs.push(new_msg_id);
}
+ created_chats.push(chat_id);
+ created_msgs.push(new_msg_id);
}
for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) {
context.emit_msgs_changed(*chat_id, *msg_id);
@@ -3485,29 +3658,31 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
msgs.push(msg)
}
- if let Some(chat_id) = chat_id {
- let chat = Chat::load_from_db(context, chat_id).await?;
- for mut msg in msgs {
- if msg.get_showpadlock() && !chat.is_protected() {
- msg.param.remove(Param::GuaranteeE2ee);
- msg.update_param(context).await?;
- }
- match msg.get_state() {
- MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
- message::update_msg_state(context, msg.id, MessageState::OutPending).await?
- }
- _ => bail!("unexpected message state"),
- }
- context.emit_event(EventType::MsgsChanged {
- chat_id: msg.chat_id,
- msg_id: msg.id,
- });
- if create_send_msg_job(context, msg.id).await?.is_some() {
- context
- .scheduler
- .interrupt_smtp(InterruptInfo::new(false))
- .await;
+ let Some(chat_id) = chat_id else {
+ return Ok(());
+ };
+
+ let chat = Chat::load_from_db(context, chat_id).await?;
+ for mut msg in msgs {
+ if msg.get_showpadlock() && !chat.is_protected() {
+ msg.param.remove(Param::GuaranteeE2ee);
+ msg.update_param(context).await?;
+ }
+ match msg.get_state() {
+ MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
+ message::update_msg_state(context, msg.id, MessageState::OutPending).await?
}
+ _ => bail!("unexpected message state"),
+ }
+ context.emit_event(EventType::MsgsChanged {
+ chat_id: msg.chat_id,
+ msg_id: msg.id,
+ });
+ if create_send_msg_job(context, &mut msg).await?.is_some() {
+ context
+ .scheduler
+ .interrupt_smtp(InterruptInfo::new(false))
+ .await;
}
}
Ok(())
@@ -3577,7 +3752,6 @@ pub async fn add_device_msg_with_importance(
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
- msg.try_calc_and_set_dimensions(context).await.ok();
prepare_msg_blob(context, msg).await?;
let timestamp_sent = create_smeared_timestamp(context);
@@ -3623,7 +3797,7 @@ pub async fn add_device_msg_with_importance(
timestamp_sent, // timestamp_sent equals timestamp_rcvd
msg.viewtype,
state,
- msg.text.as_ref().cloned().unwrap_or_default(),
+ &msg.text,
msg.param.to_string(),
rfc724_mid,
),
@@ -3797,7 +3971,7 @@ mod tests {
use crate::contact::{Contact, ContactAddress};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
- use crate::test_utils::TestContext;
+ use crate::test_utils::{TestContext, TestContextManager};
use tokio::fs;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -3860,7 +4034,7 @@ mod tests {
let t = TestContext::new().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("hello".to_string()));
+ msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
let draft = chat_id.get_draft(&t).await.unwrap().unwrap();
@@ -3875,12 +4049,12 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("hi!".to_string()));
+ msg.set_text("hi!".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert!(chat_id.get_draft(&t).await?.is_some());
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("another".to_string()));
+ msg.set_text("another".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert!(chat_id.get_draft(&t).await?.is_some());
@@ -3895,7 +4069,7 @@ mod tests {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("hello".to_string()));
+ msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
@@ -3909,7 +4083,7 @@ mod tests {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("hello".to_string()));
+ msg.set_text("hello".to_string());
assert_eq!(msg.id, MsgId::new_unset());
assert!(chat_id.get_draft_msg_id(&t).await?.is_none());
@@ -3922,7 +4096,7 @@ mod tests {
);
assert_eq!(id_after_1st_set, chat_id.get_draft(&t).await?.unwrap().id);
- msg.set_text(Some("hello2".to_string()));
+ msg.set_text("hello2".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
let id_after_2nd_set = msg.id;
@@ -3934,7 +4108,7 @@ mod tests {
let test = chat_id.get_draft(&t).await?.unwrap();
assert_eq!(id_after_2nd_set, test.id);
assert_eq!(id_after_2nd_set, msg.id);
- assert_eq!(test.text, Some("hello2".to_string()));
+ 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?;
@@ -3962,11 +4136,11 @@ mod tests {
// save a draft
let mut draft = Message::new(Viewtype::Text);
- draft.set_text(Some("draft text".to_string()));
+ draft.set_text("draft text".to_string());
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
- assert_eq!(test.text, Some("draft text".to_string()));
+ assert_eq!(test.text, "draft text".to_string());
assert!(test.quoted_text().is_none());
assert!(test.quoted_message(&t).await?.is_none());
@@ -3975,17 +4149,17 @@ mod tests {
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
- assert_eq!(test.text, Some("draft text".to_string()));
+ assert_eq!(test.text, "draft text".to_string());
assert_eq!(test.quoted_text(), Some("quote1".to_string()));
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote1.id);
// change quote on same message object
- draft.set_text(Some("another draft text".to_string()));
+ draft.set_text("another draft text".to_string());
draft.set_quote(&t, Some("e2)).await?;
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
- assert_eq!(test.text, Some("another draft text".to_string()));
+ assert_eq!(test.text, "another draft text".to_string());
assert_eq!(test.quoted_text(), Some("quote2".to_string()));
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote2.id);
@@ -3994,7 +4168,7 @@ mod tests {
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
- assert_eq!(test.text, Some("another draft text".to_string()));
+ assert_eq!(test.text, "another draft text".to_string());
assert!(test.quoted_text().is_none());
assert!(test.quoted_message(&t).await?.is_none());
@@ -4014,6 +4188,144 @@ mod tests {
assert_eq!(added, false);
}
+ /// Test adding and removing members in a group chat.
+ ///
+ /// Make sure messages sent outside contain authname
+ /// and displayed messages contain locally set name.
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_member_add_remove() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+
+ let alice = tcm.alice().await;
+ let bob = tcm.bob().await;
+
+ // Disable encryption so we can inspect raw message contents.
+ alice.set_config(Config::E2eeEnabled, Some("0")).await?;
+ bob.set_config(Config::E2eeEnabled, Some("0")).await?;
+
+ // Create contact for Bob on the Alice side with name "robert".
+ let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?;
+
+ // Set Bob authname to "Bob" and send it to Alice.
+ bob.set_config(Config::Displayname, Some("Bob")).await?;
+ tcm.send_recv(&bob, &alice, "Hello!").await;
+
+ // Check that Alice has Bob's name set to "robert" and authname set to "Bob".
+ {
+ let alice_bob_contact = Contact::get_by_id(&alice, alice_bob_contact_id).await?;
+ assert_eq!(alice_bob_contact.get_name(), "robert");
+
+ // This is the name that will be sent outside.
+ assert_eq!(alice_bob_contact.get_authname(), "Bob");
+
+ assert_eq!(alice_bob_contact.get_display_name(), "robert");
+ }
+
+ // Create and promote a group.
+ let alice_chat_id =
+ create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
+ let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
+ add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
+ let sent = alice
+ .send_text(alice_chat_id, "Hi! I created a group.")
+ .await;
+ assert!(sent.payload.contains("Hi! I created a group."));
+
+ // Alice adds Bob to the chat.
+ add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
+ let sent = alice.pop_sent_msg().await;
+ assert!(sent
+ .payload
+ .contains("I added member Bob (bob@example.net)."));
+ // Locally set name "robert" should not leak.
+ assert!(!sent.payload.contains("robert"));
+ assert_eq!(
+ sent.load_from_db().await.get_text(),
+ "You added member robert (bob@example.net)."
+ );
+
+ // Alice removes Bob from the chat.
+ remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
+ let sent = alice.pop_sent_msg().await;
+ assert!(sent
+ .payload
+ .contains("I removed member Bob (bob@example.net)."));
+ assert!(!sent.payload.contains("robert"));
+ assert_eq!(
+ sent.load_from_db().await.get_text(),
+ "You removed member robert (bob@example.net)."
+ );
+
+ // Alice leaves the chat.
+ remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
+ let sent = alice.pop_sent_msg().await;
+ assert!(sent.payload.contains("I left the group."));
+ assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
+
+ Ok(())
+ }
+
+ /// Test simultaneous removal of user from the chat and leaving the group.
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_simultaneous_member_remove() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+
+ let alice = tcm.alice().await;
+ let bob = tcm.bob().await;
+
+ alice.set_config(Config::E2eeEnabled, Some("0")).await?;
+ bob.set_config(Config::E2eeEnabled, Some("0")).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?;
+ let alice_claire_contact_id =
+ Contact::create(&alice, "Claire", "claire@example.net").await?;
+
+ // Create and promote 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?;
+ 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 adds Claire to the chat.
+ add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
+ let alice_sent_add_msg = alice.pop_sent_msg().await;
+
+ // Alice removes Bob from the chat.
+ remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
+ let alice_sent_remove_msg = alice.pop_sent_msg().await;
+
+ // Bob leaves the chat.
+ remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
+
+ // Bob receives a msg about Alice adding Claire to the group.
+ let bob_received_add_msg = bob.recv_msg(&alice_sent_add_msg).await;
+
+ // Test that add message is rewritten.
+ assert_eq!(
+ bob_received_add_msg.get_text(),
+ "Member claire@example.net added by alice@example.org."
+ );
+
+ // Bob receives a msg about Alice removing him from the group.
+ let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
+
+ // Test that remove message is rewritten.
+ assert_eq!(
+ bob_received_remove_msg.get_text(),
+ "Member Me (bob@example.net) removed by alice@example.org."
+ );
+
+ Ok(())
+ }
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_chat_multi_device() -> Result<()> {
let a1 = TestContext::new_alice().await;
@@ -4210,6 +4522,7 @@ mod tests {
Ok(())
}
+ /// Test that adding or removing contacts in 1:1 chat is not allowed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_remove_contact_for_single() {
let ctx = TestContext::new_alice().await;
@@ -4257,7 +4570,7 @@ mod tests {
t2.recv_msg(&sent_msg).await;
let chat = &t2.get_self_chat().await;
let msg = t2.get_last_msg_in(chat.id).await;
- assert_eq!(msg.text, Some("foo self".to_string()));
+ assert_eq!(msg.text, "foo self".to_string());
assert_eq!(msg.from_id, ContactId::SELF);
assert_eq!(msg.to_id, ContactId::SELF);
assert!(msg.get_showpadlock());
@@ -4271,12 +4584,12 @@ mod tests {
// add two device-messages
let mut msg1 = Message::new(Viewtype::Text);
- msg1.text = Some("first message".to_string());
+ msg1.set_text("first message".to_string());
let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await;
assert!(msg1_id.is_ok());
let mut msg2 = Message::new(Viewtype::Text);
- msg2.text = Some("second message".to_string());
+ msg2.set_text("second message".to_string());
let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await;
assert!(msg2_id.is_ok());
assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap());
@@ -4285,7 +4598,7 @@ mod tests {
let msg1 = message::Message::load_from_db(&t, msg1_id.unwrap()).await;
assert!(msg1.is_ok());
let msg1 = msg1.unwrap();
- assert_eq!(msg1.text.as_ref().unwrap(), "first message");
+ assert_eq!(msg1.text, "first message");
assert_eq!(msg1.from_id, ContactId::DEVICE);
assert_eq!(msg1.to_id, ContactId::SELF);
assert!(!msg1.is_info());
@@ -4294,7 +4607,7 @@ mod tests {
let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await;
assert!(msg2.is_ok());
let msg2 = msg2.unwrap();
- assert_eq!(msg2.text.as_ref().unwrap(), "second message");
+ assert_eq!(msg2.text, "second message");
// check device chat
assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2);
@@ -4306,13 +4619,13 @@ mod tests {
// add two device-messages with the same label (second attempt is not added)
let mut msg1 = Message::new(Viewtype::Text);
- msg1.text = Some("first message".to_string());
+ msg1.text = "first message".to_string();
let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await;
assert!(msg1_id.is_ok());
assert!(!msg1_id.as_ref().unwrap().is_unset());
let mut msg2 = Message::new(Viewtype::Text);
- msg2.text = Some("second message".to_string());
+ msg2.text = "second message".to_string();
let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await;
assert!(msg2_id.is_ok());
assert!(msg2_id.as_ref().unwrap().is_unset());
@@ -4320,7 +4633,7 @@ mod tests {
// check added message
let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await?;
assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id);
- assert_eq!(msg1.text.as_ref().unwrap(), "first message");
+ assert_eq!(msg1.text, "first message");
assert_eq!(msg1.from_id, ContactId::DEVICE);
assert_eq!(msg1.to_id, ContactId::SELF);
assert!(!msg1.is_info());
@@ -4360,7 +4673,7 @@ mod tests {
assert!(res.is_ok());
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some("message text".to_string());
+ msg.set_text("message text".to_string());
let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await;
assert!(msg_id.is_ok());
@@ -4378,7 +4691,7 @@ mod tests {
assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap());
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some("message text".to_string());
+ msg.set_text("message text".to_string());
add_device_msg(&t, Some("another-label"), Some(&mut msg))
.await
.ok();
@@ -4396,7 +4709,7 @@ mod tests {
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some("message text".to_string());
+ msg.set_text("message text".to_string());
add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.ok();
@@ -4420,7 +4733,7 @@ mod tests {
.unwrap();
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some("message text".to_string());
+ msg.set_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());
@@ -4432,7 +4745,7 @@ mod tests {
async fn test_delete_and_reset_all_device_msgs() {
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some("message text".to_string());
+ msg.set_text("message text".to_string());
let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.unwrap();
@@ -4465,7 +4778,7 @@ mod tests {
// create two chats
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some("foo".to_string());
+ msg.set_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
.await
@@ -4746,7 +5059,7 @@ mod tests {
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some("foo".to_string());
+ msg.set_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
.await
@@ -4824,7 +5137,7 @@ mod tests {
);
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("hi!".into()));
+ msg.set_text("hi!".into());
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, alice_chat_id);
@@ -4962,7 +5275,7 @@ mod tests {
let msg = t.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_chat_id(), chat_id);
assert_eq!(msg.get_viewtype(), Viewtype::Text);
- assert_eq!(msg.get_text().unwrap(), "foo info");
+ assert_eq!(msg.get_text(), "foo info");
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::Unknown);
assert!(msg.parent(&t).await?.is_none());
@@ -4989,7 +5302,7 @@ mod tests {
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.get_chat_id(), chat_id);
assert_eq!(msg.get_viewtype(), Viewtype::Text);
- assert_eq!(msg.get_text().unwrap(), "foo bar info");
+ assert_eq!(msg.get_text(), "foo bar info");
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged);
assert!(msg.parent(&t).await?.is_none());
@@ -5000,72 +5313,6 @@ mod tests {
Ok(())
}
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_set_protection() -> Result<()> {
- let t = TestContext::new_alice().await;
- t.set_config_bool(Config::BccSelf, false).await?;
- let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
- let chat = Chat::load_from_db(&t, chat_id).await?;
- assert!(!chat.is_protected());
- assert!(chat.is_unpromoted());
-
- // enable protection on unpromoted chat, the info-message is added via add_info_msg()
- chat_id
- .set_protection(&t, ProtectionStatus::Protected)
- .await?;
-
- let chat = Chat::load_from_db(&t, chat_id).await?;
- assert!(chat.is_protected());
- assert!(chat.is_unpromoted());
-
- let msgs = get_chat_msgs(&t, chat_id).await?;
- assert_eq!(msgs.len(), 1);
-
- let msg = t.get_last_msg_in(chat_id).await;
- assert!(msg.is_info());
- assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
- assert_eq!(msg.get_state(), MessageState::InNoticed);
-
- // disable protection again, still unpromoted
- chat_id
- .set_protection(&t, ProtectionStatus::Unprotected)
- .await?;
-
- let chat = Chat::load_from_db(&t, chat_id).await?;
- assert!(!chat.is_protected());
- assert!(chat.is_unpromoted());
-
- let msg = t.get_last_msg_in(chat_id).await;
- assert!(msg.is_info());
- assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled);
- assert_eq!(msg.get_state(), MessageState::InNoticed);
-
- // send a message, this switches to promoted state
- send_text_msg(&t, chat_id, "hi!".to_string()).await?;
-
- let chat = Chat::load_from_db(&t, chat_id).await?;
- assert!(!chat.is_protected());
- assert!(!chat.is_unpromoted());
-
- let msgs = get_chat_msgs(&t, chat_id).await?;
- assert_eq!(msgs.len(), 3);
-
- // enable protection on promoted chat, the info-message is sent via send_msg() this time
- chat_id
- .set_protection(&t, ProtectionStatus::Protected)
- .await?;
- let chat = Chat::load_from_db(&t, chat_id).await?;
- assert!(chat.is_protected());
- assert!(!chat.is_unpromoted());
-
- let msg = t.get_last_msg_in(chat_id).await;
- assert!(msg.is_info());
- assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
- assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
-
- Ok(())
- }
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_by_contact_id() {
let ctx = TestContext::new_alice().await;
@@ -5147,9 +5394,11 @@ mod tests {
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
- assert_eq!(msg.match_indices("Gr.").count(), 2);
+ assert_eq!(msg.match_indices("Message-ID: Result<()> {
+ async fn test_sticker(
+ filename: &str,
+ bytes: &[u8],
+ res_viewtype: Viewtype,
+ w: i32,
+ h: i32,
+ ) -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
@@ -5383,12 +5638,19 @@ mod tests {
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let mime = sent_msg.payload();
- assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
+ if res_viewtype == Viewtype::Sticker {
+ assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
+ }
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, bob_chat.id);
- assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
- assert_eq!(msg.get_filename(), Some(filename.to_string()));
+ assert_eq!(msg.get_viewtype(), res_viewtype);
+ let msg_filename = msg.get_filename().unwrap();
+ match res_viewtype {
+ Viewtype::Sticker => assert_eq!(msg_filename, filename),
+ Viewtype::Image => assert!(msg_filename.starts_with("image_")),
+ _ => panic!("Not implemented"),
+ }
assert_eq!(msg.get_width(), w);
assert_eq!(msg.get_height(), h);
assert!(msg.get_filebytes(&bob).await?.unwrap() > 250);
@@ -5400,9 +5662,10 @@ mod tests {
async fn test_sticker_png() -> Result<()> {
test_sticker(
"sticker.png",
- include_bytes!("../test-data/image/avatar64x64.png"),
- 64,
- 64,
+ include_bytes!("../test-data/image/logo.png"),
+ Viewtype::Sticker,
+ 135,
+ 135,
)
.await
}
@@ -5412,19 +5675,66 @@ mod tests {
test_sticker(
"sticker.jpg",
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
+ Viewtype::Image,
1000,
1000,
)
.await
}
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_sticker_jpeg_force() {
+ let alice = TestContext::new_alice().await;
+ let bob = TestContext::new_bob().await;
+ let alice_chat = alice.create_chat(&bob).await;
+
+ let file = alice.get_blobdir().join("sticker.jpg");
+ tokio::fs::write(
+ &file,
+ include_bytes!("../test-data/image/avatar1000x1000.jpg"),
+ )
+ .await
+ .unwrap();
+
+ // Images without force_sticker should be turned into [Viewtype::Image]
+ let mut msg = Message::new(Viewtype::Sticker);
+ msg.set_file(file.to_str().unwrap(), None);
+ let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
+ let msg = bob.recv_msg(&sent_msg).await;
+ assert_eq!(msg.get_viewtype(), Viewtype::Image);
+
+ // Images with `force_sticker = true` should keep [Viewtype::Sticker]
+ let mut msg = Message::new(Viewtype::Sticker);
+ msg.set_file(file.to_str().unwrap(), None);
+ msg.force_sticker();
+ let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
+ let msg = bob.recv_msg(&sent_msg).await;
+ assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
+
+ // Images with `force_sticker = true` should keep [Viewtype::Sticker]
+ // even on drafted messages
+ let mut msg = Message::new(Viewtype::Sticker);
+ msg.set_file(file.to_str().unwrap(), None);
+ msg.force_sticker();
+ alice_chat
+ .id
+ .set_draft(&alice, Some(&mut msg))
+ .await
+ .unwrap();
+ let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap();
+ let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
+ let msg = bob.recv_msg(&sent_msg).await;
+ assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
+ }
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_gif() -> Result<()> {
test_sticker(
"sticker.gif",
- include_bytes!("../test-data/image/image100x50.gif"),
- 100,
- 50,
+ include_bytes!("../test-data/image/logo.gif"),
+ Viewtype::Sticker,
+ 135,
+ 135,
)
.await
}
@@ -5438,8 +5748,8 @@ mod tests {
let bob_chat = bob.create_chat(&alice).await;
// create sticker
- let file_name = "sticker.jpg";
- let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
+ let file_name = "sticker.png";
+ let bytes = include_bytes!("../test-data/image/logo.png");
let file = alice.get_blobdir().join(file_name);
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(Viewtype::Sticker);
@@ -5468,7 +5778,7 @@ mod tests {
let bob_chat = bob.create_chat(&alice).await;
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("Hi Bob".to_owned()));
+ msg.set_text("Hi Bob".to_owned());
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
@@ -5476,7 +5786,7 @@ mod tests {
let forwarded_msg = bob.pop_sent_msg().await;
let msg = alice.recv_msg(&forwarded_msg).await;
- assert!(msg.get_text().unwrap() == "Hi Bob");
+ assert_eq!(msg.get_text(), "Hi Bob");
assert!(msg.is_forwarded());
Ok(())
}
@@ -5491,7 +5801,7 @@ mod tests {
add_contact_to_chat(&t, chat_id1, bob_id).await?;
let msg1 = t.get_last_msg_in(chat_id1).await;
assert!(msg1.is_info());
- assert!(msg1.get_text().unwrap().contains("bob@example.net"));
+ assert!(msg1.get_text().contains("bob@example.net"));
let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?;
assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0);
@@ -5501,7 +5811,7 @@ mod tests {
assert_eq!(msg2.get_info_type(), SystemMessage::Unknown);
assert_ne!(msg2.from_id, ContactId::INFO);
assert_ne!(msg2.to_id, ContactId::INFO);
- assert_eq!(msg2.get_text().unwrap(), msg1.get_text().unwrap());
+ assert_eq!(msg2.get_text(), msg1.get_text());
assert!(msg2.is_forwarded());
Ok(())
@@ -5520,7 +5830,7 @@ mod tests {
// Bob quotes received message and sends a reply to Alice.
let mut reply = Message::new(Viewtype::Text);
- reply.set_text(Some("Reply".to_owned()));
+ reply.set_text("Reply".to_owned());
reply.set_quote(&bob, Some(&received_msg)).await?;
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
let received_reply = alice.recv_msg(&sent_reply).await;
@@ -5571,13 +5881,13 @@ mod tests {
// Alice sends a message to Bob.
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
let received_msg = bob.recv_msg(&sent_msg).await;
- assert_eq!(received_msg.get_text(), Some("Hi Bob".to_string()));
+ assert_eq!(received_msg.get_text(), "Hi Bob");
assert_eq!(received_msg.chat_id, bob_chat.id);
// Alice sends another message to Bob, this has first message as a parent.
let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await;
let received_msg = bob.recv_msg(&sent_msg).await;
- assert_eq!(received_msg.get_text(), Some("Hello Bob".to_string()));
+ assert_eq!(received_msg.get_text(), "Hello Bob");
assert_eq!(received_msg.chat_id, bob_chat.id);
// Bob forwards message to a group chat with Alice.
@@ -5604,7 +5914,7 @@ mod tests {
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
add_contact_to_chat(&alice, group_id, bob_id).await?;
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("bla foo".to_owned()));
+ msg.set_text("bla foo".to_owned());
let sent_msg = alice.send_msg(group_id, &mut msg).await;
assert!(sent_msg.payload().contains("secretgrpname"));
assert!(sent_msg.payload().contains("secretname"));
@@ -5661,7 +5971,7 @@ mod tests {
// Bob receives all messages
let bob = TestContext::new_bob().await;
let msg = bob.recv_msg(&sent1).await;
- assert_eq!(msg.get_text().unwrap(), "alice->bob");
+ assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
bob.recv_msg(&sent2).await;
@@ -5678,7 +5988,7 @@ mod tests {
claire.configure_addr("claire@example.org").await;
claire.recv_msg(&sent2).await;
let msg = claire.recv_msg(&sent3).await;
- assert_eq!(msg.get_text().unwrap(), "alice->bob");
+ assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
@@ -5816,22 +6126,40 @@ mod tests {
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
)
.await?;
- let chat = Chat::load_from_db(&alice, broadcast_id).await?;
- assert_eq!(chat.typ, Chattype::Broadcast);
- assert_eq!(chat.name, stock_str::broadcast_list(&alice).await);
- assert!(!chat.is_self_talk());
+ set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
+ {
+ let chat = Chat::load_from_db(&alice, broadcast_id).await?;
+ assert_eq!(chat.typ, Chattype::Broadcast);
+ assert_eq!(chat.name, "Broadcast list");
+ assert!(!chat.is_self_talk());
- send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
- let msg = alice.get_last_msg().await;
- assert_eq!(msg.chat_id, chat.id);
+ send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
+ let msg = alice.get_last_msg().await;
+ assert_eq!(msg.chat_id, chat.id);
+ }
- let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
- assert_eq!(msg.get_text(), Some("ola!".to_string()));
- assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
- let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
- assert_eq!(chat.typ, Chattype::Single);
- assert_eq!(chat.id, chat_bob.id);
- assert!(!chat.is_self_talk());
+ {
+ let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
+ assert_eq!(msg.get_text(), "ola!");
+ assert_eq!(msg.subject, "Broadcast list");
+ assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
+ let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
+ assert_eq!(chat.typ, Chattype::Mailinglist);
+ assert_ne!(chat.id, chat_bob.id);
+ assert_eq!(chat.name, "Broadcast list");
+ assert!(!chat.is_self_talk());
+ }
+
+ {
+ // Alice changes the name:
+ set_chat_name(&alice, broadcast_id, "My great broadcast").await?;
+ let sent = alice.send_text(broadcast_id, "I changed the title!").await;
+
+ let msg = bob.recv_msg(&sent).await;
+ assert_eq!(msg.subject, "Re: My great broadcast");
+ let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
+ assert_eq!(bob_chat.name, "My great broadcast");
+ }
Ok(())
}
@@ -5976,7 +6304,7 @@ mod tests {
chat_id1,
Viewtype::Sticker,
"b.png",
- include_bytes!("../test-data/image/avatar64x64.png"),
+ include_bytes!("../test-data/image/logo.png"),
)
.await?;
let second_image_msg_id = send_media(
diff --git a/src/chatlist.rs b/src/chatlist.rs
index ddd60c072..883ca780c 100644
--- a/src/chatlist.rs
+++ b/src/chatlist.rs
@@ -1,6 +1,7 @@
//! # Chat list module.
use anyhow::{ensure, Context as _, Result};
+use once_cell::sync::Lazy;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
@@ -10,8 +11,14 @@ use crate::constants::{
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, MsgId};
+use crate::param::{Param, Params};
use crate::stock_str;
use crate::summary::Summary;
+use crate::tools::IsNoneOrEmpty;
+
+/// Regex to find out if a query should filter by unread messages.
+pub static IS_UNREAD_FILTER: Lazy =
+ Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
/// An object representing a single chatlist in memory.
///
@@ -76,7 +83,8 @@ impl Chatlist {
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
/// is added as needed.
/// `query`: An optional query for filtering the list. Only chats matching this query
- /// are returned.
+ /// are returned. When `is:unread` is contained in the query, the chatlist is
+ /// filtered such that only chats with unread messages show up.
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
/// are returned.
pub async fn try_load(
@@ -170,8 +178,10 @@ impl Chatlist {
)
.await?
} else if let Some(query) = query {
- let query = query.trim().to_string();
- ensure!(!query.is_empty(), "missing query");
+ let mut query = query.trim().to_string();
+ ensure!(!query.is_empty(), "query mustn't be empty");
+ let only_unread = IS_UNREAD_FILTER.find(&query).is_some();
+ query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string();
// allow searching over special names that may change at any time
// when the ui calls set_stock_translation()
@@ -196,42 +206,93 @@ impl Chatlist {
WHERE c.id>9 AND c.id!=?2
AND c.blocked!=1
AND c.name LIKE ?3
+ AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
- (MessageState::OutDraft, skip_id, str_like_cmd),
+ (MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
process_row,
process_rows,
)
.await?
} else {
- // show normal chatlist
- let sort_id_up = if flag_for_forwarding {
- ChatId::lookup_by_contact(context, ContactId::SELF)
+ let mut ids = if flag_for_forwarding {
+ let sort_id_up = ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
- .unwrap_or_default()
+ .unwrap_or_default();
+ let process_row = |row: &rusqlite::Row| {
+ let chat_id: ChatId = row.get(0)?;
+ let typ: Chattype = row.get(1)?;
+ let param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
+ let msg_id: Option = row.get(3)?;
+ Ok((chat_id, typ, param, msg_id))
+ };
+ let process_rows = |rows: rusqlite::MappedRows<_>| {
+ rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
+ Ok((chat_id, typ, param, msg_id)) => {
+ if typ == Chattype::Mailinglist
+ && param.get(Param::ListPost).is_none_or_empty()
+ {
+ None
+ } else {
+ Some(Ok((chat_id, msg_id)))
+ }
+ }
+ Err(e) => Some(Err(e)),
+ })
+ .collect::, _>>()
+ .map_err(Into::into)
+ };
+ // Return ProtectionBroken chats also, as that may happen to a verified chat at any
+ // time. It may be confusing if a chat that is normally in the list disappears
+ // suddenly. The UI need to deal with that case anyway.
+ context.sql.query_map(
+ "SELECT c.id, c.type, c.param, m.id
+ FROM chats c
+ LEFT JOIN msgs m
+ ON c.id=m.chat_id
+ AND m.id=(
+ SELECT id
+ FROM msgs
+ WHERE chat_id=c.id
+ AND (hidden=0 OR state=?)
+ ORDER BY timestamp DESC, id DESC LIMIT 1)
+ WHERE c.id>9 AND c.id!=?
+ AND c.blocked=0
+ AND NOT c.archived=?
+ AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?))
+ GROUP BY c.id
+ ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
+ (
+ MessageState::OutDraft, skip_id, ChatVisibility::Archived,
+ Chattype::Group, ContactId::SELF,
+ sort_id_up, ChatVisibility::Pinned,
+ ),
+ process_row,
+ process_rows,
+ ).await?
} else {
- ChatId::new(0)
+ // show normal chatlist
+ context.sql.query_map(
+ "SELECT c.id, m.id
+ FROM chats c
+ LEFT JOIN msgs m
+ ON c.id=m.chat_id
+ AND m.id=(
+ SELECT id
+ FROM msgs
+ WHERE chat_id=c.id
+ AND (hidden=0 OR state=?)
+ ORDER BY timestamp DESC, id DESC LIMIT 1)
+ WHERE c.id>9 AND c.id!=?
+ AND (c.blocked=0 OR c.blocked=2)
+ AND NOT c.archived=?
+ GROUP BY c.id
+ ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
+ (MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
+ process_row,
+ process_rows,
+ ).await?
};
- let mut ids = context.sql.query_map(
- "SELECT c.id, m.id
- FROM chats c
- LEFT JOIN msgs m
- ON c.id=m.chat_id
- AND m.id=(
- SELECT id
- FROM msgs
- WHERE chat_id=c.id
- AND (hidden=0 OR state=?1)
- ORDER BY timestamp DESC, id DESC LIMIT 1)
- WHERE c.id>9 AND c.id!=?2
- AND (c.blocked=0 OR (c.blocked=2 AND NOT ?3))
- AND NOT c.archived=?4
- GROUP BY c.id
- ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
- (MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned),
- process_row,
- process_rows,
- ).await?;
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
@@ -244,6 +305,27 @@ impl Chatlist {
Ok(Chatlist { ids })
}
+ /// Converts list of chat IDs to a chatlist.
+ pub(crate) async fn from_chat_ids(context: &Context, chat_ids: &[ChatId]) -> Result {
+ let mut ids = Vec::new();
+ for &chat_id in chat_ids {
+ let msg_id: Option = context
+ .sql
+ .query_get_value(
+ "SELECT id
+ FROM msgs
+ WHERE chat_id=?1
+ AND (hidden=0 OR state=?2)
+ ORDER BY timestamp DESC, id DESC LIMIT 1",
+ (chat_id, MessageState::OutDraft),
+ )
+ .await
+ .with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
+ ids.push((chat_id, msg_id));
+ }
+ Ok(Chatlist { ids })
+ }
+
/// Find out the number of chats.
pub fn len(&self) -> usize {
self.ids.len()
@@ -311,16 +393,20 @@ impl Chatlist {
};
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
- let lastmsg = Message::load_from_db(context, lastmsg_id).await?;
+ let lastmsg = Message::load_from_db(context, lastmsg_id)
+ .await
+ .context("loading message failed")?;
if lastmsg.from_id == ContactId::SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
- let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
+ let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
+ .await
+ .context("loading contact failed")?;
(Some(lastmsg), Some(lastcontact))
}
- Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
+ Chattype::Single => (Some(lastmsg), None),
}
}
} else {
@@ -362,10 +448,32 @@ pub async fn get_archived_cnt(context: &Context) -> Result {
Ok(count)
}
+/// Gets the last message of a chat, the message that would also be displayed in the ChatList
+/// Used for passing to `deltachat::chatlist::Chatlist::get_summary2`
+pub async fn get_last_message_for_chat(
+ context: &Context,
+ chat_id: ChatId,
+) -> Result> {
+ context
+ .sql
+ .query_get_value(
+ "SELECT id
+ FROM msgs
+ WHERE chat_id=?2
+ AND (hidden=0 OR state=?1)
+ ORDER BY timestamp DESC, id DESC LIMIT 1",
+ (MessageState::OutDraft, chat_id),
+ )
+ .await
+}
+
#[cfg(test)]
mod tests {
use super::*;
- use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
+ use crate::chat::{
+ add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
+ send_text_msg, ProtectionStatus,
+ };
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
@@ -373,7 +481,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() {
- let t = TestContext::new().await;
+ let t = TestContext::new_bob().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
@@ -401,7 +509,7 @@ mod tests {
// 2s here.
for chat_id in &[chat_id1, chat_id3, chat_id2] {
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("hello".to_string()));
+ msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
}
@@ -412,6 +520,31 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
assert_eq!(chats.len(), 1);
+ // receive a message from alice
+ let alice = TestContext::new_alice().await;
+ let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
+ .await
+ .unwrap();
+ add_contact_to_chat(
+ &alice,
+ alice_chat_id,
+ Contact::create(&alice, "bob", "bob@example.net")
+ .await
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+ send_text_msg(&alice, alice_chat_id, "hi".into())
+ .await
+ .unwrap();
+ let sent_msg = alice.pop_sent_msg().await;
+
+ t.recv_msg(&sent_msg).await;
+ let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
+ .await
+ .unwrap();
+ assert!(chats.len() == 1);
+
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
@@ -450,6 +583,14 @@ mod tests {
.await
.unwrap()
.is_self_talk());
+
+ remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF)
+ .await
+ .unwrap();
+ let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
+ .await
+ .unwrap();
+ assert!(chats.len() == 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -613,7 +754,7 @@ mod tests {
.unwrap();
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("foo:\nbar \r\n test".to_string()));
+ msg.set_text("foo:\nbar \r\n test".to_string());
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
diff --git a/src/config.rs b/src/config.rs
index 3527e0096..a5bf6fa70 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,6 +1,7 @@
//! # Key-value configuration management.
use std::env;
+use std::path::Path;
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result};
@@ -145,7 +146,7 @@ pub enum Config {
/// If set to "1", on the first time `start_io()` is called after configuring,
/// the newest existing messages are fetched.
/// Existing recipients are added to the contact database regardless of this setting.
- #[strum(props(default = "1"))]
+ #[strum(props(default = "0"))]
FetchExistingMsgs,
/// If set to "1", then existing messages are considered to be already fetched.
@@ -285,6 +286,12 @@ pub enum Config {
#[strum(props(default = "60"))]
ScanAllFoldersDebounceSecs,
+ /// Whether to avoid using IMAP IDLE even if the server supports it.
+ ///
+ /// This is a developer option for testing "fake idle".
+ #[strum(props(default = "0"))]
+ DisableIdle,
+
/// Defines the max. size (in bytes) of messages downloaded automatically.
/// 0 = no limit.
#[strum(props(default = "0"))]
@@ -311,6 +318,23 @@ pub enum Config {
/// Last message processed by the bot.
LastMsgId,
+
+ /// How often to gossip Autocrypt keys in chats with multiple recipients, in seconds. 2 days by
+ /// default.
+ ///
+ /// This is not supposed to be changed by UIs and only used for testing.
+ #[strum(props(default = "172800"))]
+ GossipPeriod,
+
+ /// Feature flag for verified 1:1 chats; the UI should set it
+ /// to 1 if it supports verified 1:1 chats.
+ /// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
+ /// and when the key changes, an info message is posted into the chat.
+ /// 0=Nothing else happens when the key changes.
+ /// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
+ /// until `chat_id.accept()` is called.
+ #[strum(props(default = "0"))]
+ VerifiedOneOnOneChats,
}
impl Context {
@@ -329,7 +353,11 @@ impl Context {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
- rel_path.map(|p| get_abs_path(self, p).to_string_lossy().into_owned())
+ rel_path.map(|p| {
+ get_abs_path(self, Path::new(&p))
+ .to_string_lossy()
+ .into_owned()
+ })
}
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
@@ -460,6 +488,28 @@ impl Context {
.set_raw_config(key.as_ref(), value.as_deref())
.await?;
}
+ Config::Socks5Enabled
+ | Config::BccSelf
+ | Config::E2eeEnabled
+ | Config::MdnsEnabled
+ | Config::SentboxWatch
+ | Config::MvboxMove
+ | Config::OnlyFetchMvbox
+ | Config::FetchExistingMsgs
+ | Config::DeleteToTrash
+ | Config::SaveMimeHeaders
+ | Config::Configured
+ | Config::Bot
+ | Config::NotifyAboutWrongPw
+ | Config::SendSyncMsgs
+ | Config::SignUnencrypted
+ | Config::DisableIdle => {
+ ensure!(
+ matches!(value, None | Some("0") | Some("1")),
+ "Boolean value must be either 0 or 1"
+ );
+ self.sql.set_raw_config(key.as_ref(), value).await?;
+ }
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -609,6 +659,18 @@ mod tests {
);
}
+ /// Tests that "bot" config can only be set to "0" or "1".
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_set_config_bot() {
+ let t = TestContext::new().await;
+
+ assert!(t.set_config(Config::Bot, None).await.is_ok());
+ assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
+ assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
+ assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
+ assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
+ }
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
diff --git a/src/configure.rs b/src/configure.rs
index bffdedc44..823ca302c 100644
--- a/src/configure.rs
+++ b/src/configure.rs
@@ -1,8 +1,16 @@
-//! Email accounts autoconfiguration process module.
+//! # Email accounts autoconfiguration process.
+//!
+//! The module provides automatic lookup of configuration
+//! for email providers based on the built-in [provider database],
+//! [Mozilla Thunderbird Autoconfiguration protocol]
+//! and [Outlook's Autodiscover].
+//!
+//! [provider database]: crate::provider
+//! [Mozilla Thunderbird Autoconfiguration protocol]: auto_mozilla
+//! [Outlook's Autodiscover]: auto_outlook
mod auto_mozilla;
mod auto_outlook;
-mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
@@ -18,7 +26,6 @@ use crate::config::Config;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::imap::Imap;
-use crate::job;
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
@@ -120,8 +127,8 @@ async fn on_configure_completed(
old_addr: Option,
) -> Result<()> {
if let Some(provider) = param.provider {
- if let Some(config_defaults) = &provider.config_defaults {
- for def in config_defaults.iter() {
+ if let Some(config_defaults) = provider.config_defaults {
+ for def in config_defaults {
if !context.config_exists(def.key).await? {
info!(context, "apply config_defaults {}={}", def.key, def.value);
context.set_config(def.key, Some(def.value)).await?;
@@ -136,7 +143,7 @@ async fn on_configure_completed(
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
- msg.text = Some(provider.after_login_hint.to_string());
+ msg.text = provider.after_login_hint.to_string();
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
@@ -151,7 +158,7 @@ async fn on_configure_completed(
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new(Viewtype::Text);
msg.text =
- Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
+ stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await;
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
@@ -308,7 +315,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
// respect certificate setting from function parameters
- for mut server in &mut servers {
+ for server in &mut servers {
let certificate_checks = match server.protocol {
Protocol::Imap => param.imap.certificate_checks,
Protocol::Smtp => param.smtp.certificate_checks,
@@ -452,9 +459,12 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 910);
- if ctx.get_config(Config::ConfiguredAddr).await?.as_deref() != Some(¶m.addr) {
- // Switched account, all server UIDs we know are invalid
- job::schedule_resync(ctx).await?;
+ if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? {
+ if configured_addr != param.addr {
+ // Switched account, all server UIDs we know are invalid
+ info!(ctx, "Scheduling resync because the address has changed.");
+ ctx.schedule_resync().await?;
+ }
}
// the trailing underscore is correct
@@ -643,7 +653,7 @@ async fn try_smtp_one_param(
})
} else {
info!(context, "success: {}", inf);
- smtp.disconnect().await;
+ smtp.disconnect();
Ok(())
}
}
diff --git a/src/configure/auto_mozilla.rs b/src/configure/auto_mozilla.rs
index d53b5a8c5..617429f59 100644
--- a/src/configure/auto_mozilla.rs
+++ b/src/configure/auto_mozilla.rs
@@ -1,15 +1,15 @@
//! # Thunderbird's Autoconfiguration implementation
//!
-//! Documentation:
+//! Documentation:
use std::io::BufRead;
use std::str::FromStr;
use quick_xml::events::{BytesStart, Event};
-use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::login_param::LoginParam;
+use crate::net::read_url;
use crate::provider::{Protocol, Socket};
#[derive(Debug)]
@@ -234,7 +234,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result Some(Protocol::Imap),
diff --git a/src/configure/auto_outlook.rs b/src/configure/auto_outlook.rs
index 8d42fd353..c1cfbe416 100644
--- a/src/configure/auto_outlook.rs
+++ b/src/configure/auto_outlook.rs
@@ -7,9 +7,9 @@ use std::io::BufRead;
use quick_xml::events::Event;
-use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
+use crate::net::read_url;
use crate::provider::{Protocol, Socket};
/// Result of parsing a single `Protocol` tag.
diff --git a/src/configure/read_url.rs b/src/configure/read_url.rs
deleted file mode 100644
index d164a7007..000000000
--- a/src/configure/read_url.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-use anyhow::{anyhow, format_err};
-
-use crate::context::Context;
-use crate::socks::Socks5Config;
-
-pub async fn read_url(context: &Context, url: &str) -> anyhow::Result {
- match read_url_inner(context, url).await {
- Ok(s) => {
- info!(context, "Successfully read url {}", url);
- Ok(s)
- }
- Err(e) => {
- info!(context, "Can't read URL {}: {:#}", url, e);
- Err(format_err!("Can't read URL {}: {:#}", url, e))
- }
- }
-}
-
-pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result {
- let socks5_config = Socks5Config::from_database(&context.sql).await?;
- let client = crate::http::get_client(socks5_config)?;
- let mut url = url.to_string();
-
- // Follow up to 10 http-redirects
- for _i in 0..10 {
- let response = client.get(&url).send().await?;
- if response.status().is_redirection() {
- let headers = response.headers();
- let header = headers
- .get_all("location")
- .iter()
- .last()
- .ok_or_else(|| anyhow!("Redirection doesn't have a target location"))?
- .to_str()?;
- info!(context, "Following redirect to {}", header);
- url = header.to_string();
- continue;
- }
-
- return response.text().await.map_err(Into::into);
- }
-
- Err(format_err!("Followed 10 redirections"))
-}
diff --git a/src/constants.rs b/src/constants.rs
index 5ebc47d55..165de5391 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -62,8 +62,15 @@ pub enum MediaQuality {
pub enum KeyGenType {
#[default]
Default = 0,
+
+ /// 2048-bit RSA.
Rsa2048 = 1,
+
+ /// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
Ed25519 = 2,
+
+ /// 4096-bit RSA.
+ Rsa4096 = 3,
}
/// Video chat URL type.
@@ -118,7 +125,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
/// Chat type.
#[derive(
Debug,
- Default,
Display,
Clone,
Copy,
@@ -134,10 +140,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
)]
#[repr(u32)]
pub enum Chattype {
- /// Undefined chat type.
- #[default]
- Undefined = 0,
-
/// 1:1 chat.
Single = 100,
@@ -192,11 +194,15 @@ pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// How many existing messages shall be fetched after configuration.
pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
+// max. weight of images to send w/o recoding
+pub const BALANCED_IMAGE_BYTES: usize = 500_000;
+pub const WORSE_IMAGE_BYTES: usize = 130_000;
+
// max. width/height of an avatar
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
-// max. width/height of images
+// max. width/height of images scaled down because of being too huge
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;
@@ -212,8 +218,6 @@ mod tests {
#[test]
fn test_chattype_values() {
// values may be written to disk and must not change
- assert_eq!(Chattype::Undefined, Chattype::default());
- assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap());
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
@@ -227,6 +231,7 @@ mod tests {
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
+ assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
}
#[test]
diff --git a/src/contact.rs b/src/contact.rs
index 78269f003..fed85e053 100644
--- a/src/contact.rs
+++ b/src/contact.rs
@@ -5,7 +5,7 @@ use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, ensure, Context as _, Result};
@@ -25,7 +25,7 @@ use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::context::Context;
use crate::events::EventType;
-use crate::key::{DcKey, SignedPublicKey};
+use crate::key::{load_self_public_key, DcKey};
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
@@ -109,7 +109,7 @@ impl ContactId {
/// ID of the contact for device messages.
pub const DEVICE: ContactId = ContactId::new(5);
- const LAST_SPECIAL: ContactId = ContactId::new(9);
+ pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
/// Address to go with [`ContactId::DEVICE`].
///
@@ -139,6 +139,15 @@ impl ContactId {
pub const fn to_u32(&self) -> u32 {
self.0
}
+
+ /// Mark contact as bot.
+ pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> {
+ context
+ .sql
+ .execute("UPDATE contacts SET is_bot=? WHERE id=?;", (is_bot, self.0))
+ .await?;
+ Ok(())
+ }
}
impl fmt::Display for ContactId {
@@ -223,6 +232,9 @@ pub struct Contact {
/// Last seen message signature for this contact, to be displayed in the profile.
status: String,
+
+ /// If the contact is a bot.
+ is_bot: bool,
}
/// Possible origins of a contact.
@@ -338,13 +350,35 @@ impl Default for VerifiedStatus {
}
impl Contact {
- /// Loads a contact snapshot from the database.
- pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result {
- let mut contact = context
+ /// Loads a single contact object from the database.
+ ///
+ /// Returns an error if the contact does not exist.
+ ///
+ /// For contact ContactId::SELF (1), the function returns sth.
+ /// like "Me" in the selected language and the email address
+ /// defined by set_config().
+ ///
+ /// For contact ContactId::DEVICE, the function overrides
+ /// the contact name and status with localized address.
+ pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result {
+ let contact = Self::get_by_id_optional(context, contact_id)
+ .await?
+ .with_context(|| format!("contact {contact_id} not found"))?;
+ Ok(contact)
+ }
+
+ /// Loads a single contact object from the database.
+ ///
+ /// Similar to [`Contact::get_by_id()`] but returns `None` if the contact does not exist.
+ pub async fn get_by_id_optional(
+ context: &Context,
+ contact_id: ContactId,
+ ) -> Result> {
+ if let Some(mut contact) = context
.sql
- .query_row(
+ .query_row_optional(
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
- c.authname, c.param, c.status
+ c.authname, c.param, c.status, c.is_bot
FROM contacts c
WHERE c.id=?;",
(contact_id,),
@@ -357,6 +391,7 @@ impl Contact {
let authname: String = row.get(5)?;
let param: String = row.get(6)?;
let status: Option = row.get(7)?;
+ let is_bot: bool = row.get(8)?;
let contact = Self {
id: contact_id,
name,
@@ -367,27 +402,32 @@ impl Contact {
origin,
param: param.parse().unwrap_or_default(),
status: status.unwrap_or_default(),
+ is_bot,
};
Ok(contact)
},
)
- .await?;
- if contact_id == ContactId::SELF {
- contact.name = stock_str::self_msg(context).await;
- contact.addr = context
- .get_config(Config::ConfiguredAddr)
- .await?
- .unwrap_or_default();
- contact.status = context
- .get_config(Config::Selfstatus)
- .await?
- .unwrap_or_default();
- } else if contact_id == ContactId::DEVICE {
- contact.name = stock_str::device_messages(context).await;
- contact.addr = ContactId::DEVICE_ADDR.to_string();
- contact.status = stock_str::device_messages_hint(context).await;
+ .await?
+ {
+ if contact_id == ContactId::SELF {
+ contact.name = stock_str::self_msg(context).await;
+ contact.addr = context
+ .get_config(Config::ConfiguredAddr)
+ .await?
+ .unwrap_or_default();
+ contact.status = context
+ .get_config(Config::Selfstatus)
+ .await?
+ .unwrap_or_default();
+ } else if contact_id == ContactId::DEVICE {
+ contact.name = stock_str::device_messages(context).await;
+ contact.addr = ContactId::DEVICE_ADDR.to_string();
+ contact.status = stock_str::device_messages_hint(context).await;
+ }
+ Ok(Some(contact))
+ } else {
+ Ok(None)
}
- Ok(contact)
}
/// Returns `true` if this contact is blocked.
@@ -407,7 +447,13 @@ impl Contact {
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result {
- let blocked = Self::load_from_db(context, id).await?.blocked;
+ let blocked = context
+ .sql
+ .query_row("SELECT blocked FROM contacts WHERE id=?", (id,), |row| {
+ let blocked: bool = row.get(0)?;
+ Ok(blocked)
+ })
+ .await?;
Ok(blocked)
}
@@ -466,6 +512,11 @@ impl Contact {
Ok(())
}
+ /// Returns whether contact is a bot.
+ pub fn is_bot(&self) -> bool {
+ self.is_bot
+ }
+
/// Check if an e-mail address belongs to a known and unblocked contact.
///
/// Known and unblocked contacts will be returned by `get_contacts()`.
@@ -780,7 +831,11 @@ impl Contact {
let mut ret = Vec::new();
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
-
+ let minimal_origin = if context.get_config_bool(Config::Bot).await? {
+ Origin::Unknown
+ } else {
+ Origin::IncomingReplyTo
+ };
if flag_verified_only || query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
context
@@ -800,7 +855,7 @@ impl Contact {
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
- Origin::IncomingReplyTo,
+ minimal_origin,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 }
@@ -850,10 +905,10 @@ impl Contact {
ORDER BY last_seen DESC, id DESC;",
sql::repeat_vars(self_addrs.len())
),
- rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
- ContactId::LAST_SPECIAL,
- Origin::IncomingReplyTo
- ])),
+ rusqlite::params_from_iter(
+ params_iter(&self_addrs)
+ .chain(params_slice![ContactId::LAST_SPECIAL, minimal_origin]),
+ ),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
@@ -959,7 +1014,7 @@ impl Contact {
);
let mut ret = String::new();
- if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
+ if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
let loginparam = LoginParam::load_configured_params(context).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
@@ -977,7 +1032,7 @@ impl Contact {
let finger_prints = stock_str::finger_prints(context).await;
ret += &format!("{stock_message}.\n{finger_prints}:");
- let fingerprint_self = SignedPublicKey::load_self(context)
+ let fingerprint_self = load_self_public_key(context)
.await?
.fingerprint()
.to_string();
@@ -1046,17 +1101,6 @@ impl Contact {
Ok(())
}
- /// Get a single contact object. For a list, see eg. get_contacts().
- ///
- /// For contact ContactId::SELF (1), the function returns sth.
- /// like "Me" in the selected language and the email address
- /// defined by set_config().
- pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result {
- let contact = Contact::load_from_db(context, contact_id).await?;
-
- Ok(contact)
- }
-
/// Updates `param` column in the database.
pub async fn update_param(&self, context: &Context) -> Result<()> {
context
@@ -1120,11 +1164,29 @@ impl Contact {
&self.addr
}
+ /// Get a summary of authorized name and address.
+ ///
+ /// The returned string is either "Name (email@domain.com)" or just
+ /// "email@domain.com" if the name is unset.
+ ///
+ /// This string is suitable for sending over email
+ /// as it does not leak the locally set name.
+ pub fn get_authname_n_addr(&self) -> String {
+ if !self.authname.is_empty() {
+ format!("{} ({})", self.authname, self.addr)
+ } else {
+ (&self.addr).into()
+ }
+ }
+
/// Get a summary of name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
/// "email@domain.com" if the name is unset.
///
+ /// The result should only be used locally and never sent over the network
+ /// as it leaks the local contact name.
+ ///
/// The summary is typically used when asking the user something about the contact.
/// The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?"
pub fn get_name_n_addr(&self) -> String {
@@ -1147,7 +1209,7 @@ impl Contact {
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
- return Ok(Some(get_abs_path(context, image_rel)));
+ return Ok(Some(get_abs_path(context, Path::new(image_rel))));
}
}
Ok(None)
@@ -1172,31 +1234,15 @@ impl Contact {
/// and if the key has not changed since this verification.
///
/// The UI may draw a checkbox or something like that beside verified contacts.
- ///
pub async fn is_verified(&self, context: &Context) -> Result {
- self.is_verified_ex(context, None).await
- }
-
- /// Same as `Contact::is_verified` but allows speeding up things
- /// by adding the peerstate belonging to the contact.
- /// If you do not have the peerstate available, it is loaded automatically.
- pub async fn is_verified_ex(
- &self,
- context: &Context,
- peerstate: Option<&Peerstate>,
- ) -> Result {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
if self.id == ContactId::SELF {
return Ok(VerifiedStatus::BidirectVerified);
}
- if let Some(peerstate) = peerstate {
- if peerstate.verified_key.is_some() {
- return Ok(VerifiedStatus::BidirectVerified);
- }
- } else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
- if peerstate.verified_key.is_some() {
+ if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
+ if peerstate.is_using_verified_key() {
return Ok(VerifiedStatus::BidirectVerified);
}
}
@@ -1213,11 +1259,22 @@ impl Contact {
/// Returns the ContactId that verified the contact.
pub async fn get_verifier_id(&self, context: &Context) -> Result> {
- let verifier_addr = self.get_verifier_addr(context).await?;
- if let Some(addr) = verifier_addr {
- Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
- } else {
- Ok(None)
+ let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
+ return Ok(None);
+ };
+
+ if verifier_addr == self.addr {
+ // Contact is directly verified via QR code.
+ return Ok(Some(ContactId::SELF));
+ }
+
+ match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? {
+ Some(contact_id) => Ok(Some(contact_id)),
+ None => {
+ let addr = &self.addr;
+ warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
+ Ok(None)
+ }
}
}
@@ -1317,7 +1374,7 @@ async fn set_block_contact(
contact_id
);
- let contact = Contact::load_from_db(context, contact_id).await?;
+ let contact = Contact::get_by_id(context, contact_id).await?;
if contact.blocked != new_blocking {
context
@@ -1379,7 +1436,7 @@ pub(crate) async fn set_profile_image(
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
- let mut contact = Contact::load_from_db(context, contact_id).await?;
+ let mut contact = Contact::get_by_id(context, contact_id).await?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
if contact_id == ContactId::SELF {
@@ -1434,7 +1491,7 @@ pub(crate) async fn set_status(
.await?;
}
} else {
- let mut contact = Contact::load_from_db(context, contact_id).await?;
+ let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.status != status {
contact.status = status;
@@ -1693,7 +1750,7 @@ mod tests {
assert_eq!(may_be_valid_addr("dd.tt"), false);
assert_eq!(may_be_valid_addr("tt.dd@uu"), true);
assert_eq!(may_be_valid_addr("u@d"), true);
- assert_eq!(may_be_valid_addr("u@d."), true);
+ assert_eq!(may_be_valid_addr("u@d."), false);
assert_eq!(may_be_valid_addr("u@d.t"), true);
assert_eq!(may_be_valid_addr("u@d.tt"), true);
assert_eq!(may_be_valid_addr("u@.tt"), true);
@@ -1702,6 +1759,7 @@ mod tests {
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
assert_eq!(may_be_valid_addr("as@sd.de>"), false);
assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false);
+ assert_eq!(may_be_valid_addr("user@domain.tld."), false);
}
#[test]
@@ -1752,7 +1810,7 @@ mod tests {
.await?;
assert_ne!(id, ContactId::UNDEFINED);
- let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
+ let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_authname(), "bob");
assert_eq!(contact.get_display_name(), "bob");
@@ -1780,7 +1838,7 @@ mod tests {
.await?;
assert_eq!(contact_bob_id, id);
assert_eq!(modified, Modifier::Modified);
- let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
+ let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "someone");
assert_eq!(contact.get_authname(), "bob");
assert_eq!(contact.get_display_name(), "someone");
@@ -1846,7 +1904,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_id(), contact_id);
assert_eq!(contact.get_name(), "Name one");
assert_eq!(contact.get_authname(), "bla foo");
@@ -1865,7 +1923,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Real one");
assert_eq!(contact.get_addr(), "one@eins.org");
assert!(!contact.is_blocked());
@@ -1881,7 +1939,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::None);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "three@drei.sam");
assert_eq!(contact.get_addr(), "three@drei.sam");
@@ -1898,7 +1956,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
assert!(!contact.is_blocked());
@@ -1913,7 +1971,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "m. serious");
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
assert!(!contact.is_blocked());
@@ -1929,14 +1987,14 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::None);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Wonderland, Alice");
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
assert_eq!(contact.get_addr(), "alice@w.de");
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
// check SELF
- let contact = Contact::load_from_db(&t, ContactId::SELF).await.unwrap();
+ let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
assert_eq!(contact.get_addr(), ""); // we're not configured
assert!(!contact.is_blocked());
@@ -1967,7 +2025,7 @@ mod tests {
assert_eq!(chatlist.len(), 1);
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = contacts.first().unwrap();
- let contact = Contact::load_from_db(&t, *contact_id).await?;
+ let contact = Contact::get_by_id(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "f@example.org");
@@ -1993,7 +2051,7 @@ mod tests {
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Flobbyfoo");
let chatlist = Chatlist::try_load(&t, 0, Some("flobbyfoo"), None).await?;
assert_eq!(chatlist.len(), 1);
- let contact = Contact::load_from_db(&t, *contact_id).await?;
+ let contact = Contact::get_by_id(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Flobbyfoo");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Flobbyfoo");
@@ -2023,7 +2081,7 @@ mod tests {
assert_eq!(chatlist.len(), 0);
let chatlist = Chatlist::try_load(&t, 0, Some("Foo Flobby"), None).await?;
assert_eq!(chatlist.len(), 1);
- let contact = Contact::load_from_db(&t, *contact_id).await?;
+ let contact = Contact::get_by_id(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Foo Flobby");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Foo Flobby");
@@ -2041,7 +2099,7 @@ mod tests {
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Falk");
let chatlist = Chatlist::try_load(&t, 0, Some("Falk"), None).await?;
assert_eq!(chatlist.len(), 1);
- let contact = Contact::load_from_db(&t, *contact_id).await?;
+ let contact = Contact::get_by_id(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Foo Flobby");
assert_eq!(contact.get_name(), "Falk");
assert_eq!(contact.get_display_name(), "Falk");
@@ -2080,7 +2138,7 @@ mod tests {
// If a contact has ongoing chats, contact is only hidden on deletion
Contact::delete(&alice, contact_id).await?;
- let contact = Contact::load_from_db(&alice, contact_id).await?;
+ let contact = Contact::get_by_id(&alice, contact_id).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
@@ -2094,7 +2152,7 @@ mod tests {
// Can delete contact physically now
Contact::delete(&alice, contact_id).await?;
- assert!(Contact::load_from_db(&alice, contact_id).await.is_err());
+ assert!(Contact::get_by_id(&alice, contact_id).await.is_err());
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
@@ -2113,7 +2171,7 @@ mod tests {
let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Contact::delete(&t, contact_id1).await?;
- assert!(Contact::load_from_db(&t, contact_id1).await.is_err());
+ assert!(Contact::get_by_id(&t, contact_id1).await.is_err());
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_ne!(contact_id2, contact_id1);
@@ -2122,12 +2180,12 @@ mod tests {
// test recreation after hiding
t.create_chat_with_contact("Foo", "foo@bar.de").await;
Contact::delete(&t, contact_id2).await?;
- let contact = Contact::load_from_db(&t, contact_id2).await?;
+ let contact = Contact::get_by_id(&t, contact_id2).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
- let contact = Contact::load_from_db(&t, contact_id3).await?;
+ let contact = Contact::get_by_id(&t, contact_id3).await?;
assert_eq!(contact.origin, Origin::ManuallyCreated);
assert_eq!(contact_id3, contact_id2);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
@@ -2150,7 +2208,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Created);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob1");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "bob1");
@@ -2166,7 +2224,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "bob2");
@@ -2176,7 +2234,7 @@ mod tests {
.await
.unwrap();
assert!(!contact_id.is_special());
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
@@ -2192,7 +2250,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob4");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
@@ -2205,7 +2263,7 @@ mod tests {
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t, "", "claire@example.org").await.unwrap();
assert!(!contact_id.is_special());
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire@example.org");
@@ -2221,7 +2279,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "claire1");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire1");
@@ -2237,7 +2295,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "claire2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire2");
@@ -2260,7 +2318,7 @@ mod tests {
)
.await?;
assert_eq!(sth_modified, Modifier::Created);
- let contact = Contact::load_from_db(&t, contact_id).await?;
+ let contact = Contact::get_by_id(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Bob");
// Incoming message from someone else with "Not Bob" in the "To:" field.
@@ -2273,7 +2331,7 @@ mod tests {
.await?;
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
- let contact = Contact::load_from_db(&t, contact_id).await?;
+ let contact = Contact::get_by_id(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Not Bob");
// Incoming message from Bob, changing the name back.
@@ -2286,7 +2344,7 @@ mod tests {
.await?;
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix
- let contact = Contact::load_from_db(&t, contact_id).await?;
+ let contact = Contact::get_by_id(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Bob");
Ok(())
@@ -2300,7 +2358,7 @@ mod tests {
let contact_id = Contact::create(&t, "dave1", "dave@example.org")
.await
.unwrap();
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
@@ -2314,14 +2372,14 @@ mod tests {
)
.await
.unwrap();
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// manually clear the name
Contact::create(&t, "", "dave@example.org").await.unwrap();
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "dave2");
@@ -2339,21 +2397,21 @@ mod tests {
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "", "").await.unwrap();
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t, "", "Mueller, Dave ")
.await
.unwrap();
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Mueller, Dave");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t, "name1", "name2 ")
.await
.unwrap();
- let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
+ let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "name1");
assert_eq!(contact.get_addr(), "dave@example.org");
@@ -2597,7 +2655,7 @@ CCCB 5AA9 F6E1 141C 9431
Origin::ManuallyCreated,
)
.await?;
- let contact = Contact::load_from_db(&alice, contact_id).await?;
+ let contact = Contact::get_by_id(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), 0);
let mime = br#"Subject: Hello
@@ -2614,7 +2672,7 @@ Hi."#;
let timestamp = msg.get_timestamp();
assert!(timestamp > 0);
- let contact = Contact::load_from_db(&alice, contact_id).await?;
+ let contact = Contact::get_by_id(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), timestamp);
Ok(())
diff --git a/src/context.rs b/src/context.rs
index 351a8898f..3298742d4 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -6,28 +6,26 @@ use std::future::Future;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::pin::Pin;
-use std::sync::atomic::AtomicBool;
+use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::Poll;
use std::time::{Duration, Instant, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
-use async_channel::Sender;
use ratelimit::Ratelimit;
use tokio::sync::{oneshot, Mutex, Notify, RwLock};
-use tokio::task;
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
-use crate::debug_logging::DebugEventLogData;
+use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events};
-use crate::key::{DcKey, SignedPublicKey};
+use crate::key::{load_self_public_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
-use crate::scheduler::SchedulerState;
+use crate::scheduler::{InterruptInfo, SchedulerState};
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -42,7 +40,7 @@ use crate::tools::{duration_to_str, time};
///
/// # Examples
///
-/// Creating a new unecrypted database:
+/// Creating a new unencrypted database:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
@@ -215,9 +213,6 @@ pub struct InnerContext {
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock>,
- /// Set to true if quota update is requested.
- pub(crate) quota_update_request: AtomicBool,
-
/// IMAP UID resync request.
pub(crate) resync_request: AtomicBool,
@@ -247,18 +242,10 @@ pub struct InnerContext {
pub(crate) last_error: std::sync::RwLock,
/// If debug logging is enabled, this contains all necessary information
- pub(crate) debug_logging: RwLock>,
-}
-
-#[derive(Debug)]
-pub(crate) struct DebugLogging {
- /// The message containing the logging xdc
- pub(crate) msg_id: MsgId,
- /// Handle to the background task responsible for sending
- pub(crate) loop_handle: task::JoinHandle<()>,
- /// Channel that log events should be sent to.
- /// A background loop will receive and handle them.
- pub(crate) sender: Sender,
+ ///
+ /// Standard RwLock instead of [`tokio::sync::RwLock`] is used
+ /// because the lock is used from synchronous [`Context::emit_event`].
+ pub(crate) debug_logging: std::sync::RwLock>,
}
/// The state of ongoing process.
@@ -268,7 +255,7 @@ enum RunningState {
Running { cancel_sender: oneshot::Sender<()> },
/// Cancel signal has been sent, waiting for ongoing process to be freed.
- ShallStop,
+ ShallStop { request: Instant },
/// There is no ongoing process, a new one can be allocated.
Stopped,
@@ -344,6 +331,12 @@ impl Context {
}
}
+ /// Changes encrypted database passphrase.
+ pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
+ self.sql.change_passphrase(passphrase).await?;
+ Ok(())
+ }
+
/// Returns true if database is open.
pub async fn is_open(&self) -> bool {
self.sql.is_open().await
@@ -388,16 +381,15 @@ impl Context {
translated_stockstrings: stockstrings,
events,
scheduler: SchedulerState::new(),
- ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
+ ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
quota: RwLock::new(None),
- quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
new_msgs_notify,
server_id: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()),
- debug_logging: RwLock::new(None),
+ debug_logging: std::sync::RwLock::new(None),
};
let ctx = Context {
@@ -432,6 +424,16 @@ impl Context {
self.scheduler.maybe_network().await;
}
+ pub(crate) async fn schedule_resync(&self) -> Result<()> {
+ self.resync_request.store(true, Ordering::Relaxed);
+ self.scheduler
+ .interrupt_inbox(InterruptInfo {
+ probe_network: false,
+ })
+ .await;
+ Ok(())
+ }
+
/// Returns a reference to the underlying SQL instance.
///
/// Warning: this is only here for testing, not part of the public API.
@@ -452,41 +454,18 @@ impl Context {
/// Emits a single event.
pub fn emit_event(&self, event: EventType) {
- if self
- .debug_logging
- .try_read()
- .ok()
- .map(|inner| inner.is_some())
- == Some(true)
{
- self.send_log_event(event.clone()).ok();
- };
+ let lock = self.debug_logging.read().expect("RwLock is poisoned");
+ if let Some(debug_logging) = &*lock {
+ debug_logging.log_event(event.clone());
+ }
+ }
self.events.emit(Event {
id: self.id,
typ: event,
});
}
- pub(crate) fn send_log_event(&self, event: EventType) -> anyhow::Result<()> {
- if let Ok(lock) = self.debug_logging.try_read() {
- if let Some(DebugLogging {
- msg_id: xdc_id,
- sender,
- ..
- }) = &*lock
- {
- let event_data = DebugEventLogData {
- time: time(),
- msg_id: *xdc_id,
- event,
- };
-
- sender.try_send(event_data).ok();
- }
- }
- Ok(())
- }
-
/// Emits a generic MsgsChanged event (without chat or message id)
pub fn emit_msgs_changed_without_ids(&self) {
self.emit_event(EventType::MsgsChanged {
@@ -561,14 +540,19 @@ impl Context {
let mut s = self.running_state.write().await;
// Take out the state so we can call the oneshot sender (which takes ownership).
- let current_state = std::mem::replace(&mut *s, RunningState::ShallStop);
+ let current_state = std::mem::replace(
+ &mut *s,
+ RunningState::ShallStop {
+ request: Instant::now(),
+ },
+ );
match current_state {
RunningState::Running { cancel_sender } => match cancel_sender.send(()) {
Ok(()) => info!(self, "Signaling the ongoing process to stop ASAP."),
Err(()) => warn!(self, "could not cancel ongoing"),
},
- RunningState::ShallStop | RunningState::Stopped => {
+ RunningState::ShallStop { .. } | RunningState::Stopped => {
// Put back the current state
*s = current_state;
info!(self, "No ongoing process to stop.",);
@@ -580,7 +564,7 @@ impl Context {
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
match &*self.running_state.read().await {
RunningState::Running { .. } => false,
- RunningState::ShallStop | RunningState::Stopped => true,
+ RunningState::ShallStop { .. } | RunningState::Stopped => true,
}
}
@@ -615,6 +599,7 @@ impl Context {
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
+ let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
@@ -622,7 +607,7 @@ impl Context {
.sql
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
- let fingerprint_str = match SignedPublicKey::load_self(self).await {
+ let fingerprint_str = match load_self_public_key(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!(""),
};
@@ -727,6 +712,7 @@ impl Context {
);
res.insert("bcc_self", bcc_self.to_string());
res.insert("send_sync_msgs", send_sync_msgs.to_string());
+ res.insert("disable_idle", disable_idle.to_string());
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
res.insert("fingerprint", fingerprint_str);
@@ -788,7 +774,6 @@ impl Context {
.await?
.to_string(),
);
-
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
@@ -797,6 +782,16 @@ impl Context {
"last_msg_id",
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
+ res.insert(
+ "gossip_period",
+ self.get_config_int(Config::GossipPeriod).await?.to_string(),
+ );
+ res.insert(
+ "verified_one_on_one_chats",
+ self.get_config_bool(Config::VerifiedOneOnOneChats)
+ .await?
+ .to_string(),
+ );
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
@@ -850,7 +845,22 @@ impl Context {
pub async fn get_next_msgs(&self) -> Result> {
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
Some(s) => MsgId::new(s.parse()?),
- None => MsgId::new_unset(),
+ None => {
+ // If `last_msg_id` is not set yet,
+ // subtract 1 from the last id,
+ // so a single message is returned and can
+ // be marked as seen.
+ self.sql
+ .query_row(
+ "SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
+ (),
+ |row| {
+ let msg_id: MsgId = row.get(0)?;
+ Ok(msg_id)
+ },
+ )
+ .await?
+ }
};
let list = self
@@ -1132,7 +1142,7 @@ mod tests {
async fn receive_msg(t: &TestContext, chat: &Chat) {
let members = get_chat_contacts(t, chat.id).await.unwrap();
- let contact = Contact::load_from_db(t, *members.first().unwrap())
+ let contact = Contact::get_by_id(t, *members.first().unwrap())
.await
.unwrap();
let msg = format!(
@@ -1394,11 +1404,11 @@ mod tests {
// Add messages to chat with Bob.
let mut msg1 = Message::new(Viewtype::Text);
- msg1.set_text(Some("foobar".to_string()));
+ msg1.set_text("foobar".to_string());
send_msg(&alice, chat.id, &mut msg1).await?;
let mut msg2 = Message::new(Viewtype::Text);
- msg2.set_text(Some("barbaz".to_string()));
+ msg2.set_text("barbaz".to_string());
send_msg(&alice, chat.id, &mut msg2).await?;
// Global search with a part of text finds the message.
@@ -1494,7 +1504,7 @@ mod tests {
// Add 999 messages
let mut msg = Message::new(Viewtype::Text);
- msg.set_text(Some("foobar".to_string()));
+ msg.set_text("foobar".to_string());
for _ in 0..999 {
send_msg(&alice, chat.id, &mut msg).await?;
}
@@ -1543,6 +1553,35 @@ mod tests {
Ok(())
}
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_context_change_passphrase() -> Result<()> {
+ let dir = tempdir()?;
+ let dbfile = dir.path().join("db.sqlite");
+
+ let id = 1;
+ let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
+ .await
+ .context("failed to create context")?;
+ assert_eq!(context.open("foo".to_string()).await?, true);
+ assert_eq!(context.is_open().await, true);
+
+ context
+ .set_config(Config::Addr, Some("alice@example.org"))
+ .await?;
+
+ context
+ .change_passphrase("bar".to_string())
+ .await
+ .context("Failed to change passphrase")?;
+
+ assert_eq!(
+ context.get_config(Config::Addr).await?.unwrap(),
+ "alice@example.org"
+ );
+
+ Ok(())
+ }
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ongoing() -> Result<()> {
let context = TestContext::new().await;
diff --git a/src/debug_logging.rs b/src/debug_logging.rs
index d7c93f0e4..d9a67473e 100644
--- a/src/debug_logging.rs
+++ b/src/debug_logging.rs
@@ -1,18 +1,40 @@
//! Forward log messages to logging webxdc
-use crate::{
- chat::ChatId,
- config::Config,
- context::{Context, DebugLogging},
- message::{Message, MsgId, Viewtype},
- param::Param,
- webxdc::StatusUpdateItem,
- Event, EventType,
-};
-use async_channel::{self as channel, Receiver};
+use crate::chat::ChatId;
+use crate::config::Config;
+use crate::context::Context;
+use crate::events::EventType;
+use crate::message::{Message, MsgId, Viewtype};
+use crate::param::Param;
+use crate::tools::time;
+use crate::webxdc::StatusUpdateItem;
+use async_channel::{self as channel, Receiver, Sender};
use serde_json::json;
use std::path::PathBuf;
use tokio::task;
+#[derive(Debug)]
+pub(crate) struct DebugLogging {
+ /// The message containing the logging xdc
+ pub(crate) msg_id: MsgId,
+ /// Handle to the background task responsible for sending
+ pub(crate) loop_handle: task::JoinHandle<()>,
+ /// Channel that log events should be sent to.
+ /// A background loop will receive and handle them.
+ pub(crate) sender: Sender,
+}
+
+impl DebugLogging {
+ pub(crate) fn log_event(&self, event: EventType) {
+ let event_data = DebugEventLogData {
+ time: time(),
+ msg_id: self.msg_id,
+ event,
+ };
+
+ self.sender.try_send(event_data).ok();
+ }
+}
+
/// Store all information needed to log an event to a webxdc.
pub struct DebugEventLogData {
pub time: i64,
@@ -48,12 +70,9 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver {
- context.events.emit(Event {
- id: context.id,
- typ: EventType::WebxdcStatusUpdate {
- msg_id,
- status_update_serial: serial,
- },
+ context.emit_event(EventType::WebxdcStatusUpdate {
+ msg_id,
+ status_update_serial: serial,
});
}
}
@@ -112,24 +131,26 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option) -> a
Some(msg_id.to_string().as_ref()),
)
.await?;
- let debug_logging = &mut *ctx.debug_logging.write().await;
- match debug_logging {
- // Switch logging xdc
- Some(debug_logging) => debug_logging.msg_id = msg_id,
- // Bootstrap background loop for message forwarding
- None => {
- let (sender, debug_logging_recv) = channel::bounded(1000);
- let loop_handle = {
- let ctx = ctx.clone();
- task::spawn(
- async move { debug_logging_loop(&ctx, debug_logging_recv).await },
- )
- };
- *debug_logging = Some(DebugLogging {
- msg_id,
- loop_handle,
- sender,
- });
+ {
+ let debug_logging = &mut *ctx.debug_logging.write().expect("RwLock is poisoned");
+ match debug_logging {
+ // Switch logging xdc
+ Some(debug_logging) => debug_logging.msg_id = msg_id,
+ // Bootstrap background loop for message forwarding
+ None => {
+ let (sender, debug_logging_recv) = channel::bounded(1000);
+ let loop_handle = {
+ let ctx = ctx.clone();
+ task::spawn(async move {
+ debug_logging_loop(&ctx, debug_logging_recv).await
+ })
+ };
+ *debug_logging = Some(DebugLogging {
+ msg_id,
+ loop_handle,
+ sender,
+ });
+ }
}
}
info!(ctx, "replacing logging webxdc");
@@ -139,7 +160,7 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option) -> a
ctx.sql
.set_raw_config(Config::DebugLogging.as_ref(), None)
.await?;
- *ctx.debug_logging.write().await = None;
+ *ctx.debug_logging.write().expect("RwLock is poisoned") = None;
info!(ctx, "removing logging webxdc");
}
}
diff --git a/src/decrypt.rs b/src/decrypt.rs
index 1165e3dd7..24ab873f0 100644
--- a/src/decrypt.rs
+++ b/src/decrypt.rs
@@ -13,7 +13,6 @@ use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
-use crate::keyring::Keyring;
use crate::peerstate::Peerstate;
use crate::pgp;
@@ -26,17 +25,33 @@ use crate::pgp;
pub fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
- private_keyring: &Keyring,
- public_keyring_for_validate: &Keyring,
+ private_keyring: &[SignedSecretKey],
+ public_keyring_for_validate: &[SignedPublicKey],
) -> Result, HashSet)>> {
- let encrypted_data_part = match get_autocrypt_mime(mail)
- .or_else(|| get_mixed_up_mime(mail))
- .or_else(|| get_attachment_mime(mail))
- {
+ let encrypted_data_part = match {
+ let mime = get_autocrypt_mime(mail);
+ if mime.is_some() {
+ info!(context, "Detected Autocrypt-mime message.");
+ }
+ mime
+ }
+ .or_else(|| {
+ let mime = get_mixed_up_mime(mail);
+ if mime.is_some() {
+ info!(context, "Detected mixed-up mime message.");
+ }
+ mime
+ })
+ .or_else(|| {
+ let mime = get_attachment_mime(mail);
+ if mime.is_some() {
+ info!(context, "Detected attached Autocrypt-mime message.");
+ }
+ mime
+ }) {
None => return Ok(None),
Some(res) => res,
};
- info!(context, "Detected Autocrypt-mime message");
decrypt_part(
encrypted_data_part,
@@ -211,8 +226,8 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
mail: &ParsedMail<'_>,
- private_keyring: &Keyring,
- public_keyring_for_validate: &Keyring,
+ private_keyring: &[SignedSecretKey],
+ public_keyring_for_validate: &[SignedPublicKey],
) -> Result, HashSet)>> {
let data = mail.get_body_raw()?;
@@ -247,7 +262,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts.
pub(crate) fn validate_detached_signature<'a, 'b>(
mail: &'a ParsedMail<'b>,
- public_keyring_for_validate: &Keyring,
+ public_keyring_for_validate: &[SignedPublicKey],
) -> Option<(&'a ParsedMail<'b>, HashSet)> {
if mail.ctype.mimetype != "multipart/signed" {
return None;
@@ -267,13 +282,13 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
}
}
-pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring {
- let mut public_keyring_for_validate: Keyring = Keyring::new();
+pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec {
+ let mut public_keyring_for_validate = Vec::new();
if let Some(peerstate) = peerstate {
if let Some(key) = &peerstate.public_key {
- public_keyring_for_validate.add(key.clone());
+ public_keyring_for_validate.push(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
- public_keyring_for_validate.add(key.clone());
+ public_keyring_for_validate.push(key.clone());
}
}
public_keyring_for_validate
@@ -399,8 +414,22 @@ mod tests {
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
- assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!"));
+ assert_eq!(msg.text, "Hello from Thunderbird!");
Ok(())
}
+
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_mixed_up_mime_long() -> Result<()> {
+ // Long "mixed-up" mail as received when sending an encrypted message using Delta Chat
+ // Desktop via MS Exchange (actually made with TB though).
+ let mixed_up_mime = include_bytes!("../test-data/message/mixed-up-long.eml");
+ let bob = TestContext::new_bob().await;
+ receive_imf(&bob, mixed_up_mime, false).await?;
+ let msg = bob.get_last_msg().await;
+ assert!(!msg.get_text().is_empty());
+ assert!(msg.has_html());
+ assert!(msg.id.get_html(&bob).await?.unwrap().len() > 40000);
+ Ok(())
+ }
}
diff --git a/src/dehtml.rs b/src/dehtml.rs
index 9f786a71a..02dbea4e7 100644
--- a/src/dehtml.rs
+++ b/src/dehtml.rs
@@ -10,10 +10,11 @@ use quick_xml::{
Reader,
};
-static LINE_RE: Lazy = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
+use crate::simplify::{simplify_quote, SimplifiedText};
struct Dehtml {
strbuilder: String,
+ quote: String,
add_text: AddText,
last_href: Option,
/// GMX wraps a quote in ``. After a `
`, this count is
@@ -29,17 +30,22 @@ struct Dehtml {
}
impl Dehtml {
- fn line_prefix(&self) -> &str {
- if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
- "> "
+ /// Returns true if HTML parser is currently inside the quote.
+ fn is_quote(&self) -> bool {
+ self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0
+ }
+
+ /// Returns the buffer where the text should be written.
+ ///
+ /// If the parser is inside the quote, returns the quote buffer.
+ fn get_buf(&mut self) -> &mut String {
+ if self.is_quote() {
+ &mut self.quote
} else {
- ""
+ &mut self.strbuilder
}
}
- fn append_prefix(&self, line_end: &str) -> String {
- // line_end is e.g. "\n\n". We add "> " if necessary.
- line_end.to_string() + self.line_prefix()
- }
+
fn get_add_text(&self) -> AddText {
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
AddText::No // Everything between `
` and `
` is metadata which we don't want
@@ -51,30 +57,70 @@ impl Dehtml {
#[derive(Debug, PartialEq, Clone, Copy)]
enum AddText {
+ /// Inside `