Compare commits

..

2 Commits

Author SHA1 Message Date
Septias
4b13f9352c fmt 2024-11-13 13:35:17 +01:00
Septias
37f15c5282 fmt flake with alejandra 2024-11-13 13:35:17 +01:00
133 changed files with 4056 additions and 6426 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.83.0
RUSTUP_TOOLCHAIN: 1.82.0
steps:
- uses: actions/checkout@v4
with:
@@ -37,10 +37,8 @@ jobs:
run: cargo fmt --all -- --check
- name: Run clippy
run: scripts/clippy.sh
- name: Check with all features
- name: Check
run: cargo check --workspace --all-targets --all-features
- name: Check with only default features
run: cargo check --all-targets
npm_constants:
name: Check if node constants are up to date
@@ -97,11 +95,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.83.0
rust: 1.82.0
- os: windows-latest
rust: 1.83.0
rust: 1.82.0
- os: macos-latest
rust: 1.83.0
rust: 1.82.0
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest

View File

@@ -1,104 +0,0 @@
name: Test Nix flake
on:
pull_request:
paths:
- flake.nix
- flake.lock
push:
paths:
- flake.nix
- flake.lock
branches:
- main
jobs:
format:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix fmt
# Check that formatting does not change anything.
- run: git diff --exit-code
build:
name: nix build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
installable:
# Ensure `nix develop` will work.
- devShells.x86_64-linux.default
- deltachat-python
- deltachat-repl
- deltachat-repl-aarch64-linux
- deltachat-repl-arm64-v8a-android
- deltachat-repl-armeabi-v7a-android
- deltachat-repl-armv6l-linux
- deltachat-repl-armv7l-linux
- deltachat-repl-i686-linux
- deltachat-repl-win32
- deltachat-repl-win64
- deltachat-repl-x86_64-linux
- deltachat-rpc-client
- deltachat-rpc-server
- deltachat-rpc-server-aarch64-linux
- deltachat-rpc-server-aarch64-linux-wheel
- deltachat-rpc-server-arm64-v8a-android
- deltachat-rpc-server-armeabi-v7a-android
- deltachat-rpc-server-armv6l-linux
- deltachat-rpc-server-armv6l-linux-wheel
- deltachat-rpc-server-armv7l-linux
- deltachat-rpc-server-armv7l-linux-wheel
- deltachat-rpc-server-i686-linux
- deltachat-rpc-server-i686-linux-wheel
- deltachat-rpc-server-source
- deltachat-rpc-server-win32
- deltachat-rpc-server-win32-wheel
- deltachat-rpc-server-win64
- deltachat-rpc-server-win64-wheel
- deltachat-rpc-server-x86_64-linux
- deltachat-rpc-server-x86_64-linux-wheel
- docs
- libdeltachat
- python-docs
# Fails to build
#- deltachat-repl-x86_64-android
#- deltachat-repl-x86-android
#- deltachat-rpc-server-x86_64-android
#- deltachat-rpc-server-x86-android
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build .#${{ matrix.installable }}
build-macos:
name: nix build on macOS
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
installable:
- deltachat-rpc-server-aarch64-darwin
# Fails to bulid
# - deltachat-rpc-server-x86_64-darwin
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build .#${{ matrix.installable }}

View File

@@ -1,306 +1,5 @@
# Changelog
## [1.152.2] - 2024-12-24
### Features / Changes
- Emit ImexProgress(1) after receiving backup size.
- `delete_msgs`: Use `transaction()` instead of `call_write()`.
- Start ephemeral timers when the chat is noticed.
- Start ephemeral timers when the chat is archived.
- Revalidate HTTP cache entries once per minute maximum.
### Fixes
- Reduce number of `repeat_vars()` calls.
- `sanitise_name`: Don't consider punctuation and control chars as part of file extension ([#6362](https://github.com/deltachat/deltachat-core-rust/pull/6362)).
### Refactor
- Remove marknoticed_chat_if_older_than().
### Miscellaneous Tasks
- Remove contrib/ directory.
## [1.152.1] - 2024-12-17
### Build system
- Downgrade Rust version used to build binaries.
- Reduce MSRV to 1.77.0.
## [1.152.0] - 2024-12-12
### API-Changes
- [**breaking**] Remove `dc_prepare_msg` and `dc_msg_is_increation`.
### Build system
- Increase MSRV to 1.81.0.
### Features / Changes
- Cache HTTP GET requests.
- Prefix server-url in info.
- Set `mime_modified` for the last message part, not the first ([#4462](https://github.com/deltachat/deltachat-core-rust/pull/4462)).
### Fixes
- Render "message" parts in multipart messages' HTML ([#4462](https://github.com/deltachat/deltachat-core-rust/pull/4462)).
- Ignore garbage at the end of the keys.
## [1.151.6] - 2024-12-11
### Features / Changes
- Don't add "Failed to send message to ..." info messages to group chats.
- Add info messages about implicit membership changes if group member list is recreated ([#6314](https://github.com/deltachat/deltachat-core-rust/pull/6314)).
### Fixes
- Add self-addition message to chat when recreating member list.
- Do not subscribe to heartbeat if already subscribed via metadata.
### Build system
- Add idna 0.5.0 exception into deny.toml.
### Documentation
- Update links to Node.js bindings in the README.
### Refactor
- Factor out `wait_for_all_work_done()`.
### Tests
- Notifiy more prominently & in more tests about false positives when running `cargo test` ([#6308](https://github.com/deltachat/deltachat-core-rust/pull/6308)).
## [1.151.5] - 2024-12-05
### API-Changes
- [**breaking**] Remove dc_all_work_done().
### Security
- cargo: Update rPGP to 0.14.2.
This fixes [Panics on Malformed Untrusted Input](https://github.com/rpgp/rpgp/security/advisories/GHSA-9rmp-2568-59rv)
and [Potential Resource Exhaustion when handling Untrusted Messages](https://github.com/rpgp/rpgp/security/advisories/GHSA-4grw-m28r-q285).
This allows the attacker to crash the application via specially crafted messages and keys.
We recommend all users and bot operators to upgrade to the latest version.
There is no impact on the confidentiality of the messages and keys so no action other than upgrading is needed.
### Fixes
- Store plaintext in mime_headers of truncated sent messages ([#6273](https://github.com/deltachat/deltachat-core-rust/pull/6273)).
### Documentation
- Document `push` module.
- Remove mention of non-existent `nightly` feature.
### Tests
- Fix panic in `receive_emails` benchmark ([#6306](https://github.com/deltachat/deltachat-core-rust/pull/6306)).
## [1.151.4] - 2024-12-03
### Features / Changes
- Encrypt notification tokens.
### Fixes
- Replace connectivity state "Connected" with "Preparing".
### Miscellaneous Tasks
- Beta clippy suggestions ([#6271](https://github.com/deltachat/deltachat-core-rust/pull/6271)).
### Tests
- Fix `cargo check` for `receive_emails` benchmark.
### CI
- Also run cargo check without all-features.
## [1.151.3] - 2024-12-02
### API-Changes
- Remove experimental `request_internet_access` option from webxdc's `manifest.toml`.
- Add getWebxdcHref to json api ([#6281](https://github.com/deltachat/deltachat-core-rust/pull/6281)).
### CI
- Update Rust to 1.83.0.
### Documentation
- Update dc_msg_get_info_type() and dc_get_securejoin_qr() ([#6269](https://github.com/deltachat/deltachat-core-rust/pull/6269)).
- Fix references to iroh-related headers in peer_channels docs.
- Improve CFFI docs, link to corresponding JSON-RPC docs.
### Features / Changes
- Allow the user to replace maps integration ([#5678](https://github.com/deltachat/deltachat-core-rust/pull/5678)).
- Mark saved messages chat as protected.
### Fixes
- Close iroh endpoint when I/O is stopped.
- Do not add protection messages to Saved Messages chat.
- Mark Saved Messages chat as protected if it exists.
- Sync chat action even if sync message arrives before first one from contact ([#6259](https://github.com/deltachat/deltachat-core-rust/pull/6259)).
### Refactor
- Remove some .unwrap() calls.
- Create_status_update_record: Remove double check of info_msg_id.
- Use Option::or_else() to dedup emitting IncomingWebxdcNotify.
## [1.151.2] - 2024-11-26
### API-Changes
- Deprecate webxdc `descr` parameter ([#6255](https://github.com/deltachat/deltachat-core-rust/pull/6255)).
### Features / Changes
- AEAP: Check that the old peerstate verified key fingerprint hasn't changed when removing it.
- Add `AccountsChanged` and `AccountsItemChanged` events ([#6118](https://github.com/deltachat/deltachat-core-rust/pull/6118)).
- Do not use format=flowed in outgoing messages ([#6256](https://github.com/deltachat/deltachat-core-rust/pull/6256)).
- Add webxdc limits api.
- Add href to IncomingWebxdcNotify event ([#6266](https://github.com/deltachat/deltachat-core-rust/pull/6266)).
### Fixes
- Revert treating some transient SMTP errors as permanent.
### Refactor
- Create_status_update_record: Get rid of `notify` var.
### Tests
- Check that IncomingMsg isn't emitted for reactions.
## [1.151.1] - 2024-11-24
### Build system
- nix: Fix deltachat-rpc-server-source installable.
### CI
- Test building nix targets to avoid regressions.
## [1.151.0] - 2024-11-23
### Features / Changes
- Trim whitespace from scanned QR codes.
- Use privacy-preserving webxdc addresses ([#6237](https://github.com/deltachat/deltachat-core-rust/pull/6237)).
- Webxdc notify ([#6230](https://github.com/deltachat/deltachat-core-rust/pull/6230)).
- `update.href` api ([#6248](https://github.com/deltachat/deltachat-core-rust/pull/6248)).
### Fixes
- Never notify SELF ([#6251](https://github.com/deltachat/deltachat-core-rust/pull/6251)).
### Build system
- Use underscores in deltachat-rpc-server source package filename.
- Remove imap_tools from dependencies ([#6238](https://github.com/deltachat/deltachat-core-rust/pull/6238)).
- cargo: Update Rustls from 0.23.14 to 0.23.18.
- deps: Bump curve25519-dalek from 3.2.0 to 4.1.3 in /fuzz.
### Documentation
- Move style guide into a separate document.
- Clarify DC_EVENT_INCOMING_WEBXDC_NOTIFY documentation ([#6249](https://github.com/deltachat/deltachat-core-rust/pull/6249)).
### Tests
- After AEAP, 1:1 chat isn't available for sending, but unprotected groups are ([#6222](https://github.com/deltachat/deltachat-core-rust/pull/6222)).
## [1.150.0] - 2024-11-21
### API-Changes
- Correct `DC_CERTCK_ACCEPT_*` values and docs ([#6176](https://github.com/deltachat/deltachat-core-rust/pull/6176)).
### Features / Changes
- Use Rustls for connections with strict TLS ([#6186](https://github.com/deltachat/deltachat-core-rust/pull/6186)).
- Experimental header protection for Autocrypt.
- Tune down io-not-started info in connectivity-html.
- Clear config cache in start_io() ([#6228](https://github.com/deltachat/deltachat-core-rust/pull/6228)).
- Line-before-quote may be up to 120 character long instead of 80.
- Use i.delta.chat in qr codes ([#6223](https://github.com/deltachat/deltachat-core-rust/pull/6223)).
### Fixes
- Prevent accidental wrong-password-notifications ([#6122](https://github.com/deltachat/deltachat-core-rust/pull/6122)).
- Remove footers from "Show Full Message...".
- `send_msg_to_smtp`: Return Ok if `smtp` row is deleted in parallel.
- Only add "member added/removed" messages if they actually do that ([#5992](https://github.com/deltachat/deltachat-core-rust/pull/5992)).
- Do not fail to load chatlist summary if the message got removed.
- deltachat-jsonrpc: Do not fail `get_chatlist_items_by_entries` if the message got deleted.
- deltachat-jsonrpc: Do not fail `get_draft` if draft is deleted.
- `markseen_msgs`: Limit not yet downloaded messages state to `InNoticed` ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)).
- Update state of message when fully downloading it.
- Dont overwrite equal drafts ([#6212](https://github.com/deltachat/deltachat-core-rust/pull/6212)).
### Build system
- Silence RUSTSEC-2024-0384.
- cargo: Update rPGP from 0.13.2 to 0.14.0.
- cargo: Update futures-concurrency from 7.6.1 to 7.6.2.
- Update flake.nix ([#6200](https://github.com/deltachat/deltachat-core-rust/pull/6200))
### CI
- Ensure flake is formatted.
### Documentation
- Scanned proxies are added and normalized.
### Refactor
- Fix nightly clippy warnings.
- Remove slicing from `is_file_in_use`.
- Remove unnecessary `allow(clippy::indexing_slicing)`.
- Don't use slicing in `remove_nonstandard_footer`.
- Do not use slicing in `qr` module.
- Eliminate indexing in `compute_mailinglist_name`.
- Remove unused `allow(clippy::indexing_slicing)`.
- Remove indexing/slicing from `remove_message_footer`.
- Remove indexing/slicing from `squash_attachment_parts`.
- Remove unused allow(clippy::indexing_slicing) for heuristically_parse_ndn.
- Remove indexing/slicing from `parse_message_ids`.
- Remove slicing from `remove_bottom_quote`.
- Get rid of slicing in `remove_top_quote`.
- Remove unused allow(clippy::indexing_slicing) from 'truncate'.
- Forbid clippy::indexing_slicing.
- Forbid clippy::string_slice.
- Delete chat in a transaction.
- Fix typo in `context.rs`.
### Tests
- Remove all calls to print() from deltachat-rpc-client tests.
- Reply to protected group from MUA.
- Mark not downloaded message as seen ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)).
- Mark `receive_imf()` as only for tests and "internals" feature ([#6235](https://github.com/deltachat/deltachat-core-rust/pull/6235)).
## [1.149.0] - 2024-11-05
### Build system
@@ -537,7 +236,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
- Reset quota on configured address change ([#5908](https://github.com/deltachat/deltachat-core-rust/pull/5908)).
- Do not emit progress 1000 when configuration is cancelled.
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/deltachat/deltachat-core-rust/pull/5338)).
- Re-add tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
- Readd tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
### Miscellaneous Tasks
@@ -1128,7 +827,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
### Tests
- deltachat-rpc-client: re-enable `log_cli`.
- deltachat-rpc-client: reenable `log_cli`.
## [1.140.0] - 2024-06-04
@@ -2065,7 +1764,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
- Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol.
- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat.
- deltachat-repl: Enable INFO logging by default and add timestamps.
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elements based on the configuration key which is a part of the event.
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elments based on the configuration key which is a part of the event.
- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)).
- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)).
- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)).
@@ -5530,14 +5229,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.148.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.5..v1.148.6
[1.148.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.6..v1.148.7
[1.149.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.7..v1.149.0
[1.150.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.149.0..v1.150.0
[1.151.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.150.0..v1.151.0
[1.151.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.0..v1.151.1
[1.151.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.1..v1.151.2
[1.151.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.2..v1.151.3
[1.151.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.3..v1.151.4
[1.151.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.4..v1.151.5
[1.151.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.5..v1.151.6
[1.152.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.6..v1.152.0
[1.152.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.0..v1.152.1
[1.152.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.1..v1.152.2

View File

@@ -1,6 +1,6 @@
# Contributing to Delta Chat
# Contributing guidelines
## Bug reports
## Reporting bugs
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
If the bug you found is specific to
@@ -9,114 +9,178 @@ If the bug you found is specific to
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
report it to the corresponding repository.
## Feature proposals
## Proposing features
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
## Code contributions
## Contributing code
If you want to contribute a code, follow this guide.
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
1. **Select an issue to work on.**
If you have an write access to the repository, assign the issue to yourself.
Otherwise state in the comment that you are going to work on the issue
to avoid duplicate work.
If the issue does not exist yet, create it first.
2. **Write the code.**
Follow the [coding conventions](STYLE.md) when writing the code.
3. **Commit the code.**
If you have write access to the repository,
push a branch named `<username>/<feature>`
so it is clear who is responsible for the branch,
and open a PR proposing to merge the change.
Otherwise fork the repository and create a branch in your fork.
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
The following prefix types are used:
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
- `test`: Test changes and improvements to the testing framework.
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
Release preparation commits are marked as "chore(release): prepare for X.Y.Z"
as described in [releasing guide](RELEASE.md).
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
Alternatively, breaking changes can go into the commit description, e.g.:
```
fix: Fix race condition and db corruption when a message was received during backup
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
```
4. [**Open a Pull Request**](https://github.com/deltachat/deltachat-core-rust/pulls).
Refer to the corresponding issue.
If you intend to squash merge the PR from the web interface,
make sure the PR title follows the conventional commits notation
as it will end up being a commit title.
Otherwise make sure each commit title follows the conventional commit notation.
5. **Make sure all CI checks succeed.**
CI runs the tests and checks code formatting.
While it is running, self-review your PR to make sure all the changes you expect are there
and there are no accidentally committed unrelated changes and files.
Push the necessary fixup commits or force-push to your branch if needed.
6. **Ask for review.**
Use built-in GitHub feature to request a review from suggested reviewers.
If you do not have write access to the repository, ask for review in the comments.
7. **Merge the PR.**
Once a PR has an approval and passes CI, it can be merged.
PRs from a branch created in the main repository,
i.e. authored by those who have write access, are merged by their authors.
This is to ensure that PRs are merged as intended by the author,
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
If you have multiple changes in one PR, do a rebase merge.
Otherwise, you should usually do a squash merge.
If PR author does not have write access to the repository,
maintainers who reviewed the PR can merge it.
If you do not have access to the repository and created a PR from a fork,
ask the maintainers to merge the PR and say how it should be merged.
## Other ways to contribute
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
If you have write access to the repository,
push a branch named `<username>/<feature>`
so it is clear who is responsible for the branch,
and open a PR proposing to merge the change.
Otherwise fork the repository and create a branch in your fork.
You can find the list of good first issues
and a link to this guide
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
### Coding conventions
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
### SQL
Multi-line SQL statements should be formatted using string literals,
for example
```
sql.execute(
"CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '' NOT NULL -- message text
) STRICT",
)
.await?;
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
or [`indoc!](https://docs.rs/indoc).
Do not escape newlines like this:
```
sql.execute(
"CREATE TABLE messages ( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
text TEXT DEFAULT '' NOT NULL \
) STRICT",
)
.await?;
```
Escaping newlines
is prone to errors like this if space before backslash is missing:
```
"SELECT foo\
FROM bar"
```
Literal above results in `SELECT fooFROM bar` string.
This style also does not allow using `--` comments.
---
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
to make SQLite check column types.
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
This avoids reuse of the row IDs and can avoid dangerous bugs
like forwarding wrong message because the message was deleted
and another message took its row ID.
Declare all new columns as `NOT NULL`
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
Use `HAVING COUNT(*) > 0` clause
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
an older version. Also don't change the column type, consider adding a new column with another name
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
keyword doesn't help here.
### Commit messages
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
The following prefix types are used:
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
- `test`: Test changes and improvements to the testing framework.
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
If you intend to squash merge the PR from the web interface,
make sure the PR title follows the conventional commits notation
as it will end up being a commit title.
Otherwise make sure each commit title follows the conventional commit notation.
#### Breaking Changes
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
Alternatively, breaking changes can go into the commit description, e.g.:
```
fix: Fix race condition and db corruption when a message was received during backup
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
```
#### Multiple Changes in one PR
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
[Clippy]: https://doc.rust-lang.org/clippy/
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/
### Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
capitalize it but do not add a full stop as the contexts will be separated by `:`.
For example:
```
.with_context(|| format!("Unable to trash message {msg_id}"))
```
All errors should be handled in one of these ways:
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
- With `.log_err().ok()`.
- Bubbled up with `?`.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`
and get a backtrace with line numbers in resultified tests
which return `anyhow::Result`.
### Logging
For logging, use `info!`, `warn!` and `error!` macros.
Log messages should be capitalized and have a full stop in the end. For example:
```
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
```
Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
### Reviewing
Once a PR has an approval and passes CI, it can be merged.
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
This is to ensure that PRs are merged as intended by the author,
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
If you do not have access to the repository and created a PR from a fork,
ask the maintainers to merge the PR and say how it should be merged.
## Other ways to contribute
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).

547
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -14,8 +14,8 @@ For example, to release version 1.116.0 of the core, do the following steps.
5. Commit the changes as `chore(release): prepare for 1.116.0`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
6. Tag the release: `git tag --annotate v1.116.0`.
6. Tag the release: `git tag -a v1.116.0`.
7. Push the release tag: `git push origin v1.116.0`.
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.

View File

@@ -1,98 +0,0 @@
# Coding conventions
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
[Clippy]: https://doc.rust-lang.org/clippy/
## SQL
Multi-line SQL statements should be formatted using string literals,
for example
```
sql.execute(
"CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '' NOT NULL -- message text
) STRICT",
)
.await?;
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
or [`indoc!](https://docs.rs/indoc).
Do not escape newlines like this:
```
sql.execute(
"CREATE TABLE messages ( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
text TEXT DEFAULT '' NOT NULL \
) STRICT",
)
.await?;
```
Escaping newlines
is prone to errors like this if space before backslash is missing:
```
"SELECT foo\
FROM bar"
```
Literal above results in `SELECT fooFROM bar` string.
This style also does not allow using `--` comments.
---
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
to make SQLite check column types.
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
This avoids reuse of the row IDs and can avoid dangerous bugs
like forwarding wrong message because the message was deleted
and another message took its row ID.
Declare all new columns as `NOT NULL`
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
Use `HAVING COUNT(*) > 0` clause
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
an older version. Also don't change the column type, consider adding a new column with another name
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
keyword doesn't help here.
## Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
capitalize it but do not add a full stop as the contexts will be separated by `:`.
For example:
```
.with_context(|| format!("Unable to trash message {msg_id}"))
```
All errors should be handled in one of these ways:
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
- With `.log_err().ok()`.
- Bubbled up with `?`.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`
and get a backtrace with line numbers in resultified tests
which return `anyhow::Result`.
## Logging
For logging, use `info!`, `warn!` and `error!` macros.
Log messages should be capitalized and have a full stop in the end. For example:
```
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
```
Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```

View File

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

80
contrib/proxy.py Normal file
View File

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

View File

@@ -15,8 +15,7 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.152.2"
version = "1.149.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -418,7 +418,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts
* default=send and request read receipts, only send but not request if `bot` is set
* default=send and request read receipts, only send but not reuqest if `bot` is set
* - `bcc_self` = 0=do not send a copy of outgoing messages to self,
* 1=send a copy of outgoing messages to self (default).
* Sending messages to self is needed for a proper multi-account setup,
@@ -722,6 +722,12 @@ char* dc_get_connectivity_html (dc_context_t* context);
int dc_get_push_state (dc_context_t* context);
/**
* Only used by the python tests.
*/
int dc_all_work_done (dc_context_t* context);
// connect
/**
@@ -963,6 +969,54 @@ uint32_t dc_create_chat_by_contact_id (dc_context_t* context, uint32_t co
uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t contact_id);
/**
* Prepare a message for sending.
*
* Call this function if the file to be sent is still in creation.
* Once you're done with creating the file, call dc_send_msg() as usual
* and the message will really be sent.
*
* This is useful as the user can already send the next messages while
* e.g. the recoding of a video is not yet finished. Or the user can even forward
* the message with the file being still in creation to other groups.
*
* Files being sent with the increation-method must be placed in the
* blob directory, see dc_get_blobdir().
* If the increation-method is not used - which is probably the normal case -
* dc_send_msg() copies the file to the blob directory if it is not yet there.
* To distinguish the two cases, msg->state must be set properly. The easiest
* way to ensure this is to re-use the same object for both calls.
*
* Example:
* ~~~
* char* blobdir = dc_get_blobdir(context);
* char* file_to_send = mprintf("%s/%s", blobdir, "send.mp4")
*
* dc_msg_t* msg = dc_msg_new(context, DC_MSG_VIDEO);
* dc_msg_set_file(msg, file_to_send, NULL);
* dc_prepare_msg(context, chat_id, msg);
*
* // ... create the file ...
*
* dc_send_msg(context, chat_id, msg);
*
* dc_msg_unref(msg);
* free(file_to_send);
* dc_str_unref(file_to_send);
* ~~~
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID to send the message to.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id and state of the object are set up,
* The function does not take ownership of the object,
* so you have to free it using dc_msg_unref() as usual.
* @return The ID of the message that is being prepared.
*/
uint32_t dc_prepare_msg (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
/**
* Send a message defined by a dc_msg_t object to a chat.
*
@@ -987,11 +1041,13 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co
* If that fails, is not possible, or the image is already small enough, the image is sent as original.
* If you want images to be always sent as the original file, use the #DC_MSG_FILE type.
*
* Videos and other file types are currently not recoded by the library.
* Videos and other file types are currently not recoded by the library,
* with dc_prepare_msg(), however, you can do that from the UI.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID to send the message to.
* If dc_prepare_msg() was called before, this parameter can be 0.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id of the object is set up,
* The function does not take ownership of the object,
@@ -1008,6 +1064,7 @@ uint32_t dc_send_msg (dc_context_t* context, uint32_t ch
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID to send the message to.
* If dc_prepare_msg() was called before, this parameter can be 0.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id of the object is set up,
* The function does not take ownership of the object,
@@ -1097,14 +1154,9 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the message with the webxdc instance.
* @param json program-readable data, this is created in JS land as:
* - `payload`: any JS object or primitive.
* - `info`: optional informational message. Will be shown in chat and may be added as system notification.
* note that also users that are not notified explicitly get the `info` or `summary` update shown in the chat.
* - `document`: optional document name. shown eg. in title bar.
* - `summary`: optional summary. shown beside app icon.
* - `notify`: optional array of other users `selfAddr` to be notified e.g. by a sound about `info` or `summary`.
* @param descr Deprecated, set to NULL
* @param json program-readable data, the actual payload
* @param descr The user-visible description of JSON data,
* in case of a chess game, e.g. the move.
* @return 1=success, 0=error
*/
int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const char* json, const char* descr);
@@ -2489,8 +2541,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_PROXY with dc_lot_t::text1=address:
* ask the user if they want to use the given proxy.
* if so, call dc_set_config_from_qr() and restart I/O.
* On success, dc_get_config(context, "proxy_url")
* will contain the new proxy in normalized form as the first element.
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* e-mail address scanned, optionally, a draft message could be set in
@@ -2540,15 +2590,13 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* The returned text will also work as a normal https:-link,
* so that the QR code is useful also without Delta Chat being installed
* or can be passed to contacts through other channels.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id If set to a group-chat-id,
@@ -3934,7 +3982,7 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
*
* Outgoing message states:
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
* the message enters this state before @ref DC_STATE_OUT_PENDING. Deprecated.
* the message enters this state before @ref DC_STATE_OUT_PENDING.
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
@@ -4144,13 +4192,9 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
* - internet_access:
* true if the Webxdc should get internet access;
* this is the case i.e. for experimental maps integration.
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
* Should be exposed to `webxdc.sendUpdateMaxSize` in JS land.
* true if the Webxdc should get full internet access, including Webrtc.
* currently, this is only true for encrypted Webxdc's in the self chat
* that have requested internet access in the manifest.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
@@ -4436,7 +4480,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
*
* Even when you display an icon,
* you should still display the text of the informational message using dc_msg_get_text()
@@ -4466,23 +4509,19 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
/**
* Get link attached to an webxdc info message.
* The info message needs to be of type DC_INFO_WEBXDC_INFO_MESSAGE.
* Check if a message is still in creation. A message is in creation between
* the calls to dc_prepare_msg() and dc_send_msg().
*
* Typically, this is used to set `document.location.href` in JS land.
*
* Webxdc apps can define the link by setting `update.href` when sending and update,
* see dc_send_webxdc_status_update().
* Typically, this is used for videos that are recoded by the UI before
* they can be sent.
*
* @memberof dc_msg_t
* @param msg The info message object.
* Not: the webxdc instance.
* @return The link to be set to `document.location.href` in JS land.
* Returns NULL if there is no link attached to the info message and on errors.
* @param msg The message object.
* @return 1=message is still in creation (dc_send_msg() was not called yet),
* 0=message no longer in creation.
*/
char* dc_msg_get_webxdc_href (const dc_msg_t* msg);
int dc_msg_is_increation (const dc_msg_t* msg);
/**
@@ -4635,7 +4674,7 @@ int dc_msg_has_html (dc_msg_t* msg);
* If the download fails or succeeds,
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
*
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any further download action.
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any futher download action.
* It was fully downloaded, but we failed to decrypt it.
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
*
@@ -5393,8 +5432,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Message containing a sticker, similar to image.
* NB: When sending, the message viewtype may be changed to `Image` by some heuristics like checking
* for transparent pixels.
* If possible, the UI should display the image without borders in a transparent way.
* A click on a sticker will offer to install the sticker set in some future.
*/
@@ -5499,8 +5536,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Outgoing message being prepared. See dc_msg_get_state() for details.
*
* @deprecated 2024-12-07
*/
#define DC_STATE_OUT_PREPARING 18
@@ -5674,14 +5709,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_CERTCK_STRICT 1
/**
* Accept certificates that are expired, self-signed
* or not valid for the server hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID 2
/**
* For API compatibility only: Treat this as DC_CERTCK_ACCEPT_INVALID on reading.
* Must not be written.
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID_CERTIFICATES 3
@@ -5721,23 +5750,6 @@ void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
* returns immediately and once the result is ready it can be retrieved via dc_jsonrpc_next_response()
* the jsonrpc specification defines an invocation id that can then be used to match request and response.
*
* An overview of JSON-RPC calls is available at
* <https://js.jsonrpc.delta.chat/classes/RawClient.html>.
* Note that the page describes only the rough methods.
* Calling convention, casing etc. does vary, this is a known flaw,
* and at some point we will get to improve that :)
*
* Also, note that most calls are more high-level than this CFFI, require more database calls and are slower.
* They're more suitable for an environment that is totally async and/or cannot use CFFI, which might not be true for native apps.
*
* Notable exceptions that exist only as JSON-RPC and probably never get a CFFI counterpart:
* - getMessageReactions(), sendReaction()
* - getHttpResponse()
* - draftSelfReport()
* - getAccountFileSize()
* - importVcard(), parseVcard(), makeVcard()
* - sendWebxdcRealtimeData, sendWebxdcRealtimeAdvertisement(), leaveWebxdcRealtime()
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param request JSON-RPC request as string
@@ -5758,8 +5770,6 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Make a JSON-RPC call and return a response.
*
* See dc_jsonrpc_request() for an overview of possible calls and for more information.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param input JSON-RPC request.
@@ -5855,26 +5865,15 @@ int dc_event_get_data2_int(dc_event_t* event);
/**
* Get data associated with an event object.
* The meaning of the data depends on the event ID returned as @ref DC_EVENT constants.
* The meaning of the data depends on the event ID
* returned as @ref DC_EVENT constants by dc_event_get_id().
* See also dc_event_get_data1_int() and dc_event_get_data2_int().
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data1" string or NULL.
* The meaning depends on the event type associated with this event.
* Must be freed using dc_str_unref().
*/
char* dc_event_get_data1_str(dc_event_t* event);
/**
* Get data associated with an event object.
* The meaning of the data depends on the event ID returned as @ref DC_EVENT constants.
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data2" string or NULL.
* The meaning depends on the event type associated with this event.
* Must be freed using dc_str_unref().
* @return "data2" as a string or NULL.
* the meaning depends on the event type associated with this event.
* Once you're done with the string, you have to unref it using dc_unref_str().
*/
char* dc_event_get_data2_str(dc_event_t* event);
@@ -6072,35 +6071,12 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_INCOMING_REACTION 2002
/**
* A webxdc wants an info message or a changed summary to be notified.
*
* @param data1 (int) contact_id ID _and_ (char*) href.
* - dc_event_get_data1_int() returns contact_id of the sending contact.
* - dc_event_get_data1_str() returns the href as set to `update.href`.
* @param data2 (int) msg_id _and_ (char*) text_to_notify.
* - dc_event_get_data2_int() returns the msg_id,
* referring to the webxdc-info-message, if there is any.
* Sometimes no webxdc-info-message is added to the chat
* and yet a notification is sent; in this case the msg_id
* of the webxdc instance is returned.
* - dc_event_get_data2_str() returns text_to_notify,
* the text that shall be shown in the notification.
* string must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_INCOMING_WEBXDC_NOTIFY 2003
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
*
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
*
* If the message is a webxdc info message,
* dc_msg_get_parent() returns the webxdc instance the notification belongs to.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
*/
@@ -6384,25 +6360,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
/**
* Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
*
* This event is only emitted by the account manager.
*/
#define DC_EVENT_ACCOUNTS_CHANGED 2302
/**
* Inform that an account property that might be shown in the account list changed, namely:
* - is_configured (see dc_is_configured())
* - displayname
* - selfavatar
* - private_tag
*
* This event is emitted from the account whose property changed.
*/
#define DC_EVENT_ACCOUNTS_ITEM_CHANGED 2303
/**
* Inform that some events have been skipped due to event channel overflow.
@@ -6850,7 +6807,7 @@ void dc_event_unref(dc_event_t* event);
/// "Failed to send message to %1$s."
///
/// Unused. Was used in group chat status messages.
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact the message cannot be sent to
#define DC_STR_FAILED_SENDING_TO 74

View File

@@ -413,6 +413,16 @@ pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc
block_on(ctx.push_state()) as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_all_work_done()");
return 0;
}
let ctx = &*context;
block_on(async move { ctx.all_work_done().await as libc::c_int })
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_oauth2_url(
context: *mut dc_context_t,
@@ -532,7 +542,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingReaction { .. } => 2002,
EventType::IncomingWebxdcNotify { .. } => 2003,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
@@ -559,8 +568,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::AccountsBackgroundFetchDone => 2200,
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
EventType::AccountsChanged => 2302,
EventType::AccountsItemChanged => 2303,
EventType::EventChannelOverflow { .. } => 2400,
#[allow(unreachable_patterns)]
#[cfg(test)]
@@ -593,12 +600,9 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ConfigSynced { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged => 0,
EventType::IncomingReaction { contact_id, .. }
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
| EventType::AccountsBackgroundFetchDone => 0,
EventType::ChatlistChanged => 0,
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -670,8 +674,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::ChatlistItemChanged { .. }
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::ConfigSynced { .. }
| EventType::ChatModified(_)
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
@@ -679,7 +681,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
| EventType::IncomingWebxdcNotify { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -699,27 +700,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_event_get_data1_str(event: *mut dc_event_t) -> *mut libc::c_char {
if event.is_null() {
eprintln!("ignoring careless call to dc_event_get_data1_str()");
return ptr::null_mut();
}
let event = &(*event).typ;
match event {
EventType::IncomingWebxdcNotify { href, .. } => {
if let Some(href) = href {
href.to_c_string().unwrap_or_default().into_raw()
} else {
ptr::null_mut()
}
}
_ => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut libc::c_char {
if event.is_null() {
@@ -768,8 +748,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
@@ -797,9 +775,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
.to_c_string()
.unwrap_or_default()
.into_raw(),
EventType::IncomingWebxdcNotify { text, .. } => {
text.to_c_string().unwrap_or_default().into_raw()
}
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -976,6 +951,27 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_prepare_msg(
context: *mut dc_context_t,
chat_id: u32,
msg: *mut dc_msg_t,
) -> u32 {
if context.is_null() || chat_id == 0 || msg.is_null() {
eprintln!("ignoring careless call to dc_prepare_msg()");
return 0;
}
let ctx = &mut *context;
let ffi_msg: &mut MessageWrapper = &mut *msg;
block_on(async move {
chat::prepare_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message)
.await
.unwrap_or_log_default(ctx, "Failed to prepare message")
})
.to_u32()
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_msg(
context: *mut dc_context_t,
@@ -1063,7 +1059,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
msg_id: u32,
json: *const libc::c_char,
_descr: *const libc::c_char,
descr: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_webxdc_status_update()");
@@ -1071,10 +1067,14 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
}
let ctx = &*context;
block_on(ctx.send_webxdc_status_update(MsgId::new(msg_id), &to_string_lossy(json)))
.context("Failed to send webxdc update")
.log_err(ctx)
.is_ok() as libc::c_int
block_on(ctx.send_webxdc_status_update(
MsgId::new(msg_id),
&to_string_lossy(json),
&to_string_lossy(descr),
))
.context("Failed to send webxdc update")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -3682,14 +3682,13 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char {
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_webxdc_href()");
return "".strdup();
eprintln!("ignoring careless call to dc_msg_is_increation()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_webxdc_href().strdup()
ffi_msg.message.is_increation().into()
}
#[no_mangle]

View File

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

View File

@@ -1135,11 +1135,9 @@ impl CommandApi {
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
let message_object = MessageObject::from_msg_id(&ctx, msg_id)
MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))?
.with_context(|| format!("Message {msg_id} does not exist for account {account_id}"))?;
Ok(message_object)
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
@@ -1163,10 +1161,7 @@ impl CommandApi {
messages.insert(
message_id,
match message_result {
Ok(Some(message)) => MessageLoadResult::Message(message),
Ok(None) => MessageLoadResult::LoadingError {
error: "Message does not exist".to_string(),
},
Ok(message) => MessageLoadResult::Message(message),
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
@@ -1767,10 +1762,10 @@ impl CommandApi {
account_id: u32,
instance_msg_id: u32,
update_str: String,
_descr: Option<String>,
description: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str)
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
.await
}
@@ -1829,18 +1824,6 @@ impl CommandApi {
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
}
/// Get href from a WebxdcInfoMessage which might include a hash holding
/// information about a specific position or state in a webxdc app (optional)
async fn get_webxdc_href(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
Ok(message.get_webxdc_href())
}
/// Get blob encoded as base64 from a webxdc message
///
/// path is the path of the file within webxdc archive
@@ -2016,7 +1999,9 @@ impl CommandApi {
async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result<Option<MessageObject>> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
Ok(MessageObject::from_msg_id(&ctx, draft.get_id()).await?)
Ok(Some(
MessageObject::from_msg_id(&ctx, draft.get_id()).await?,
))
} else {
Ok(None)
}
@@ -2185,9 +2170,7 @@ impl CommandApi {
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id)
.await?
.context("Just sent message does not exist")?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
Ok((msg_id.to_u32(), message))
}

View File

@@ -88,17 +88,11 @@ pub(crate) async fn get_chat_list_item_by_id(
let (last_updated, message_type) = match last_msgid {
Some(id) => {
if let Some(last_message) =
deltachat::message::Message::load_from_db_optional(ctx, id).await?
{
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
} else {
// Message may be deleted by the time we try to load it.
(None, None)
}
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
}
None => (None, None),
};

View File

@@ -69,7 +69,7 @@ pub enum EventType {
/// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a message box then.
/// in a messasge box then.
Error { msg: String },
/// An action cannot be performed because the user is not in the group.
@@ -106,16 +106,6 @@ pub enum EventType {
reaction: String,
},
/// Incoming webxdc info or summary update, should be notified.
#[serde(rename_all = "camelCase")]
IncomingWebxdcNotify {
chat_id: u32,
contact_id: u32,
msg_id: u32,
text: String,
href: Option<String>,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -287,20 +277,6 @@ pub enum EventType {
#[serde(rename_all = "camelCase")]
ChatlistItemChanged { chat_id: Option<u32> },
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
///
/// This event is only emitted by the account manager
AccountsChanged,
/// Inform that an account property that might be shown in the account list changed, namely:
/// - is_configured (see is_configured())
/// - displayname
/// - selfavatar
/// - private_tag
///
/// This event is emitted from the account whose property changed.
AccountsItemChanged,
/// Inform than some events have been skipped due to event channel overflow.
EventChannelOverflow { n: u64 },
}
@@ -343,19 +319,6 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
reaction: reaction.as_str().to_string(),
},
CoreEventType::IncomingWebxdcNotify {
chat_id,
contact_id,
msg_id,
text,
href,
} => IncomingWebxdcNotify {
chat_id: chat_id.to_u32(),
contact_id: contact_id.to_u32(),
msg_id: msg_id.to_u32(),
text,
href,
},
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
@@ -446,8 +409,6 @@ impl From<CoreEventType> for EventType {
},
CoreEventType::ChatlistChanged => ChatlistChanged,
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
CoreEventType::AccountsChanged => AccountsChanged,
CoreEventType::AccountsItemChanged => AccountsItemChanged,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),

View File

@@ -85,8 +85,6 @@ pub struct MessageObject {
webxdc_info: Option<WebxdcMessageInfo>,
webxdc_href: Option<String>,
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
@@ -114,10 +112,8 @@ enum MessageQuote {
}
impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Option<Self>> {
let Some(message) = Message::load_from_db_optional(context, msg_id).await? else {
return Ok(None);
};
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let sender_contact = Contact::get_by_id(context, message.get_from_id())
.await
@@ -187,7 +183,7 @@ impl MessageObject {
.map(Into::into)
.collect();
let message_object = MessageObject {
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
@@ -243,17 +239,12 @@ impl MessageObject {
file_name: message.get_filename(),
webxdc_info,
// On a WebxdcInfoMessage this might include a hash holding
// information about a specific position or state in a webxdc app
webxdc_href: message.get_webxdc_href(),
download_state,
reactions,
vcard_contact: vcard_contacts.first().cloned(),
};
Ok(Some(message_object))
})
}
}
@@ -273,9 +264,6 @@ pub enum MessageViewtype {
Gif,
/// Message containing a sticker, similar to image.
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
///
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker,

View File

@@ -35,14 +35,6 @@ pub struct WebxdcMessageInfo {
source_code_url: Option<String>,
/// True if full internet access should be granted to the app.
internet_access: bool,
/// Address to be used for `window.webxdc.selfAddr` in JS land.
self_addr: String,
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
/// Should be exposed to `window.sendUpdateInterval` in JS land.
send_update_interval: usize,
/// Maximum number of bytes accepted for a serialized update object.
/// Should be exposed to `window.sendUpdateMaxSize` in JS land.
send_update_max_size: usize,
}
impl WebxdcMessageInfo {
@@ -57,11 +49,7 @@ impl WebxdcMessageInfo {
document,
summary,
source_code_url,
request_integration: _,
internet_access,
self_addr,
send_update_interval,
send_update_max_size,
} = message.get_webxdc_info(context).await?;
Ok(Self {
@@ -71,9 +59,6 @@ impl WebxdcMessageInfo {
summary: maybe_empty_string_to_option(summary),
source_code_url: maybe_empty_string_to_option(source_code_url),
internet_access,
self_addr,
send_update_interval,
send_update_max_size,
})
}
}

View File

@@ -1,6 +1,4 @@
#![recursion_limit = "256"]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
pub mod api;
pub use yerpc;

View File

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

View File

@@ -90,11 +90,6 @@ impl Ratelimit {
pub fn until_can_send(&self) -> Duration {
self.until_can_send_at(SystemTime::now())
}
/// Returns minimum possible update interval in milliseconds.
pub fn update_interval(&self) -> usize {
(self.window.as_millis() as f64 / self.quota) as usize
}
}
#[cfg(test)]
@@ -107,7 +102,6 @@ mod tests {
let mut ratelimit = Ratelimit::new_at(Duration::new(60, 0), 3.0, now);
assert!(ratelimit.can_send_at(now));
assert_eq!(ratelimit.update_interval(), 20_000);
// Send burst of 3 messages.
ratelimit.send_at(now);

View File

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

View File

@@ -969,7 +969,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"Arguments <msg-id> <json status update> expected"
);
let msg_id = MsgId::new(arg1.parse()?);
context.send_webxdc_status_update(msg_id, arg2).await?;
context
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
.await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");

View File

@@ -25,8 +25,7 @@ $ pip install .
## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Install tox `pip install -U tox`
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.152.2"
version = "1.149.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -24,6 +24,9 @@ classifiers = [
"Topic :: Communications :: Email"
]
readme = "README.md"
dependencies = [
"imap-tools",
]
[tool.setuptools.package-data]
deltachat_rpc_client = [

View File

@@ -41,7 +41,6 @@ class EventType(str, Enum):
REACTIONS_CHANGED = "ReactionsChanged"
INCOMING_MSG = "IncomingMsg"
INCOMING_MSG_BUNCH = "IncomingMsgBunch"
INCOMING_REACTION = "IncomingReaction"
MSGS_NOTICED = "MsgsNoticed"
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"
@@ -62,8 +61,6 @@ class EventType(str, Enum):
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
ACCOUNTS_CHANGED = "AccountsChanged"
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"

View File

@@ -1,3 +1,7 @@
"""
Internal Python-level IMAP handling used by the tests.
"""
from __future__ import annotations
import imaplib
@@ -7,11 +11,17 @@ import ssl
from contextlib import contextmanager
from typing import TYPE_CHECKING
import pytest
from imap_tools import AND, Header, MailBox, MailMessage, MailMessageFlags, errors
from imap_tools import (
AND,
Header,
MailBox,
MailMessage,
MailMessageFlags,
errors,
)
if TYPE_CHECKING:
from deltachat_rpc_client import Account
from . import Account
FLAGS = b"FLAGS"
FETCH = b"FETCH"
@@ -19,8 +29,6 @@ ALL = "1:*"
class DirectImap:
"""Internal Python-level IMAP handling."""
def __init__(self, account: Account) -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
@@ -204,8 +212,3 @@ class IdleManager:
def done(self):
"""send idle-done to server if we are currently in idle mode."""
return self.direct_imap.conn.idle.stop()
@pytest.fixture
def direct_imap():
return DirectImap

View File

@@ -1,29 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from deltachat_rpc_client import EventType
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import ACFactory
def test_event_on_configuration(acfactory: ACFactory) -> None:
"""
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
"""
account = acfactory.new_preconfigured_account()
account.clear_all_events()
assert not account.is_configured()
future = account.configure.future()
while True:
event = account.wait_for_event()
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
break
assert account.is_configured()
future()
# other tests are written in rust: src/tests/account_events.rs

View File

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

View File

@@ -12,6 +12,7 @@ import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.direct_imap import DirectImap
from deltachat_rpc_client.rpc import JsonRpcError
@@ -535,7 +536,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
def test_reactions_for_a_reordering_move(acfactory):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
@@ -559,7 +560,7 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap = DirectImap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")

View File

@@ -24,9 +24,6 @@ def test_webxdc(acfactory) -> None:
"name": "Chess Board",
"sourceCodeUrl": None,
"summary": None,
"selfAddr": webxdc_info["selfAddr"],
"sendUpdateInterval": 1000,
"sendUpdateMaxSize": 18874368,
}
status_updates = message.get_webxdc_status_updates()

View File

@@ -16,7 +16,6 @@ deps =
pytest
pytest-timeout
pytest-xdist
imap-tools
[testenv:lint]
skipsdist = True

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,9 +18,6 @@ ignore = [
# Unmaintained instant
"RUSTSEC-2024-0384",
# idna 0.5.0
"RUSTSEC-2024-0421",
]
[bans]
@@ -36,11 +33,9 @@ skip = [
{ name = "event-listener", version = "2.5.3" },
{ name = "event-listener", version = "4.0.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "fiat-crypto", version = "0.1.20" },
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "http", version = "0.2.12" },
{ name = "idna", version = "0.5.0" },
{ name = "nix", version = "0.26.4" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
@@ -80,7 +75,6 @@ allow = [
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
]

62
flake.lock generated
View File

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

382
flake.nix
View File

@@ -1,20 +1,25 @@
{
description = "Delta Chat core";
inputs = {
# Old Rust to build releases.
fenix.url = "github:nix-community/fenix?rev=85f4139f3c092cf4afd9f9906d7ed218ef262c97";
# New Rust for development shell.
new-fenix.url = "github:nix-community/fenix";
fenix.url = "github:nix-community/fenix";
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
nix-filter.url = "github:numtide/nix-filter";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
android.url = "github:tadfisher/android-nixpkgs";
};
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, new-fenix, android }:
flake-utils.lib.eachDefaultSystem (system:
outputs =
{ self
, nixpkgs
, flake-utils
, nix-filter
, naersk
, fenix
, android
,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs.stdenv) isDarwin;
@@ -178,15 +183,17 @@
# Use DWARF-2 instead of SJLJ for exception handling.
winCC = pkgsWin32.buildPackages.wrapCC (
(pkgsWin32.buildPackages.gcc-unwrapped.override
({
{
threadsCross = {
model = "win32";
package = null;
};
})).overrideAttrs (oldAttr: {
configureFlags = oldAttr.configureFlags ++ [
"--disable-sjlj-exceptions --with-dwarf2"
];
}).overrideAttrs (oldAttr: {
configureFlags =
oldAttr.configureFlags
++ [
"--disable-sjlj-exceptions --with-dwarf2"
];
})
);
in
@@ -330,227 +337,216 @@
{
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
"deltachat-rpc-server-${arch}" = rpc-server;
"deltachat-rpc-server-${arch}-wheel" =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
"deltachat-rpc-server-${arch}-wheel" = pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
};
in
{
formatter = pkgs.nixpkgs-fmt;
packages =
mkRustPackages "aarch64-linux" //
mkRustPackages "i686-linux" //
mkRustPackages "x86_64-linux" //
mkRustPackages "armv7l-linux" //
mkRustPackages "armv6l-linux" //
mkRustPackages "x86_64-darwin" //
mkRustPackages "aarch64-darwin" //
mkAndroidPackages "armeabi-v7a" //
mkAndroidPackages "arm64-v8a" //
mkAndroidPackages "x86" //
mkAndroidPackages "x86_64" // rec {
mkRustPackages "aarch64-linux"
// mkRustPackages "i686-linux"
// mkRustPackages "x86_64-linux"
// mkRustPackages "armv7l-linux"
// mkRustPackages "armv6l-linux"
// mkAndroidPackages "armeabi-v7a"
// mkAndroidPackages "arm64-v8a"
// mkAndroidPackages "x86"
// mkAndroidPackages "x86_64"
// rec {
# Run with `nix run .#deltachat-repl foo.db`.
deltachat-repl = mkRustPackage "deltachat-repl";
deltachat-rpc-server = mkRustPackage "deltachat-rpc-server";
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win64-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win64-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
deltachat-rpc-server-win64-wheel = pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win64-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
buildInputs = [
deltachat-rpc-server-win64
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win64
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win32-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win32-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
deltachat-rpc-server-win32-wheel = pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win32-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
buildInputs = [
deltachat-rpc-server-win32
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win32
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
# Run `nix build .#docs` to get C docs generated in `./result/`.
docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
docs = pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
libdeltachat =
pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
libdeltachat = pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
# Source package for deltachat-rpc-server.
# Fake package that downloads Linux version,
# needed to install deltachat-rpc-server on Android with `pip`.
deltachat-rpc-server-source =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-source";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat_rpc_server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-server-source = pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-source";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-client =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
};
deltachat-rpc-client = pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
};
deltachat-python =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-python";
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
libdeltachat
];
nativeBuildInputs = [
pkgs.pkg-config
];
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.pkgconfig
pkgs.python3Packages.cffi
pkgs.python3Packages.imap-tools
pkgs.python3Packages.pluggy
pkgs.python3Packages.requests
];
};
python-docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
deltachat-python = pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-python";
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
libdeltachat
];
nativeBuildInputs = [
pkgs.pkg-config
];
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.pkgconfig
pkgs.python3Packages.cffi
pkgs.python3Packages.imap-tools
pkgs.python3Packages.pluggy
pkgs.python3Packages.requests
];
};
python-docs = pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
};
devShells.default =
let
pkgs = import nixpkgs {
system = system;
overlays = [ new-fenix.overlays.default ];
overlays = [ fenix.overlays.default ];
};
in
pkgs.mkShell {
buildInputs = with pkgs; [
(new-fenix.packages.${system}.complete.withComponents [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"

View File

@@ -8,8 +8,6 @@
//! is assumed to be set to "no".
//!
//! For received messages, DelSp parameter is honoured.
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
/// Wraps line to 72 characters using format=flowed soft breaks.
///

2609
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
// Generated!
module.exports = {
DC_CERTCK_ACCEPT_INVALID: 2,
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
DC_CERTCK_AUTO: 0,
DC_CERTCK_STRICT: 1,
@@ -31,8 +30,6 @@ module.exports = {
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_DOWNLOAD_UNDECIPHERABLE: 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
DC_EVENT_ACCOUNTS_CHANGED: 2302,
DC_EVENT_ACCOUNTS_ITEM_CHANGED: 2303,
DC_EVENT_CHANNEL_OVERFLOW: 2400,
DC_EVENT_CHATLIST_CHANGED: 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
@@ -54,7 +51,6 @@ module.exports = {
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
DC_EVENT_INCOMING_REACTION: 2002,
DC_EVENT_INCOMING_WEBXDC_NOTIFY: 2003,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,

View File

@@ -17,7 +17,6 @@ module.exports = {
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
@@ -44,7 +43,5 @@ module.exports = {
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2302: 'DC_EVENT_ACCOUNTS_CHANGED',
2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
}

View File

@@ -1,7 +1,6 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID = 2,
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
@@ -31,8 +30,6 @@ export enum C {
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_DOWNLOAD_UNDECIPHERABLE = 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
DC_EVENT_ACCOUNTS_CHANGED = 2302,
DC_EVENT_ACCOUNTS_ITEM_CHANGED = 2303,
DC_EVENT_CHANNEL_OVERFLOW = 2400,
DC_EVENT_CHATLIST_CHANGED = 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
@@ -54,7 +51,6 @@ export enum C {
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
DC_EVENT_INCOMING_REACTION = 2002,
DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
@@ -328,7 +324,6 @@ export const EventId2EventName: { [key: number]: string } = {
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
@@ -355,7 +350,5 @@ export const EventId2EventName: { [key: number]: string } = {
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2302: 'DC_EVENT_ACCOUNTS_CHANGED',
2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.152.2"
version = "1.149.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1366,9 +1366,10 @@ def test_quote_encrypted(acfactory, lp):
msg_draft.quote = quoted_msg
chat.set_draft(msg_draft)
# Get the draft and send it.
# Get the draft, prepare and send it.
msg_draft = chat.get_draft()
chat.send_msg(msg_draft)
msg_out = chat.prepare_message(msg_draft)
chat.send_prepared(msg_out)
chat.set_draft(None)
assert chat.get_draft() is None
@@ -1898,11 +1899,10 @@ def test_connectivity(acfactory, lp):
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `DC_CONNECTIVITY_CONNECTED`, "
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
"all messages are fetched",
)
@@ -1911,7 +1911,7 @@ def test_connectivity(acfactory, lp):
ac2.create_chat(ac1).send_text("Hi")
idle1.wait_for_new_message()
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_all_work_done()
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1
assert msgs[0].text == "Hi"
@@ -1927,6 +1927,30 @@ def test_connectivity(acfactory, lp):
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages")
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked")
ac1.create_contact(ac2).block()
ac1.direct_imap.select_config_folder("inbox")
with ac1.direct_imap.idle() as idle1:
ac2.create_chat(ac1).send_text("Hi")
idle1.wait_for_new_message()
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
ac1.set_config("configured_mail_pw", "abc")
@@ -1937,6 +1961,32 @@ def test_connectivity(acfactory, lp):
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_all_work_done(acfactory, lp):
"""
Tests that calling start_io() immediately followed by maybe_network()
and then waiting for all_work_done() reliably fetches the messages
delivered while account was offline.
In other words, connectivity should not change to a state
where all_work_done() returns true until IMAP connection goes idle.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.direct_imap.select_config_folder("inbox")
with ac1.direct_imap.idle() as idle1:
ac2.create_chat(ac1).send_text("Hi")
idle1.wait_for_new_message()
ac1.start_io()
ac1.maybe_network()
ac1._evtracker.wait_for_all_work_done()
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1
assert msgs[0].text == "Hi"
def test_fetch_deleted_msg(acfactory, lp):
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
hundreds of times, because uid_next was not updated.
@@ -2290,8 +2340,9 @@ def test_group_quote(acfactory, lp):
reply_msg = Message.new_empty(msg.chat.account, "text")
reply_msg.set_text("reply")
reply_msg.quote = msg
reply_msg = msg.chat.prepare_message(reply_msg)
assert reply_msg.quoted_text == "hello"
msg.chat.send_msg(reply_msg)
msg.chat.send_prepared(reply_msg)
lp.sec("ac3: receiving reply")
received_reply = ac3._evtracker.wait_next_incoming_message()

View File

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

View File

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

View File

@@ -216,7 +216,7 @@ def test_logged_ac_process_ffi_failure(acfactory):
ac1.add_account_plugin(FailPlugin())
# cause any event eg contact added/changed
ac1.create_contact("something@example.org")
res = cap.get(timeout=600)
res = cap.get(timeout=10)
assert "ac_process_ffi_event" in res
assert "ZeroDivisionError" in res
assert "Traceback" in res

View File

@@ -1 +1 @@
2024-12-24
2024-11-05

View File

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

View File

@@ -25,7 +25,7 @@ def build_source_package(version, filename):
def pack(name, contents):
contents = contents.encode()
tar_info = tarfile.TarInfo(f"deltachat_rpc_server-{version}/{name}")
tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}")
tar_info.mode = 0o644
tar_info.size = len(contents)
pkg.addfile(tar_info, BytesIO(contents))
@@ -167,7 +167,7 @@ def main():
cargo_manifest = tomllib.load(fp)
version = cargo_manifest["package"]["version"]
if sys.argv[1] == "source":
filename = f"deltachat_rpc_server-{version}.tar.gz"
filename = f"deltachat-rpc-server-{version}.tar.gz"
build_source_package(version, filename)
else:
arch = sys.argv[1]

View File

@@ -139,7 +139,6 @@ impl Accounts {
ctx.open("".to_string()).await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
Ok(account_config.id)
}
@@ -157,7 +156,6 @@ impl Accounts {
.build()
.await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
Ok(account_config.id)
}
@@ -192,7 +190,6 @@ impl Accounts {
.context("failed to remove account data")?;
}
self.config.remove_account(id).await?;
self.emit_event(EventType::AccountsChanged);
Ok(())
}

View File

@@ -260,6 +260,7 @@ fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str>
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use tokio::fs;
use tokio::io::AsyncReadExt;

View File

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

View File

@@ -28,7 +28,7 @@ use crate::contact::{self, Contact, ContactId, Origin};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer};
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::location;
@@ -567,14 +567,6 @@ impl ChatId {
contact_id: Option<ContactId>,
timestamp_sort: i64,
) -> Result<()> {
if contact_id == Some(ContactId::SELF) {
// Do not add protection messages to Saved Messages chat.
// This chat never gets protected and unprotected,
// we do not want the first message
// to be a protection message with an arbitrary timestamp.
return Ok(());
}
let text = context.stock_protection_msg(protect, contact_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
@@ -688,10 +680,6 @@ impl ChatId {
})
.await?;
if visibility == ChatVisibility::Archived {
start_chat_ephemeral_timers(context, self).await?;
}
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, self);
@@ -747,7 +735,7 @@ impl ChatId {
.await?;
if unread_cnt == 1 {
// Added the first unread message in the chat.
context.emit_msgs_changed_without_msg_id(DC_CHAT_ID_ARCHIVED_LINK);
context.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
}
return Ok(());
}
@@ -762,8 +750,6 @@ impl ChatId {
/// shown.
pub(crate) fn emit_msg_event(self, context: &Context, msg_id: MsgId, important: bool) {
if important {
debug_assert!(!msg_id.is_unset());
context.emit_incoming_msg(self, msg_id);
} else {
context.emit_msgs_changed(self, msg_id);
@@ -779,19 +765,27 @@ impl ChatId {
);
let chat = Chat::load_from_db(context, self).await?;
context
.sql
.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
(self,),
)
.await?;
context
.sql
.transaction(|transaction| {
transaction.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
(self,),
)?;
transaction.execute("DELETE FROM msgs WHERE chat_id=?", (self,))?;
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (self,))?;
transaction.execute("DELETE FROM chats WHERE id=?", (self,))?;
Ok(())
})
.execute("DELETE FROM msgs WHERE chat_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM chats WHERE id=?;", (self,))
.await?;
context.emit_msgs_changed_without_ids();
@@ -825,14 +819,17 @@ impl ChatId {
};
if changed {
if msg.is_some() {
match self.get_draft_msg_id(context).await? {
Some(msg_id) => context.emit_msgs_changed(self, msg_id),
None => context.emit_msgs_changed_without_msg_id(self),
}
} else {
context.emit_msgs_changed_without_msg_id(self)
}
context.emit_msgs_changed(
self,
if msg.is_some() {
match self.get_draft_msg_id(context).await? {
Some(msg_id) => msg_id,
None => MsgId::new(0),
}
} else {
MsgId::new(0)
},
);
}
Ok(())
@@ -891,7 +888,7 @@ impl ChatId {
_ => {
let blob = msg
.param
.get_blob(Param::File, context)
.get_blob(Param::File, context, !msg.is_increation())
.await?
.context("no file stored in params")?;
msg.param.set(Param::File, blob.as_name());
@@ -925,27 +922,24 @@ impl ChatId {
&& old_draft.chat_id == self
&& old_draft.state == MessageState::OutDraft
{
let affected_rows = context
.sql.execute(
"UPDATE msgs
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
WHERE id=?7
AND (type <> ?2
OR txt <> ?3
OR txt_normalized <> ?4
OR param <> ?5
OR mime_in_reply_to <> ?6);",
(
time(),
msg.viewtype,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
),
).await?;
return Ok(affected_rows > 0);
context
.sql
.execute(
"UPDATE msgs
SET timestamp=?,type=?,txt=?,txt_normalized=?,param=?,mime_in_reply_to=?
WHERE id=?;",
(
time(),
msg.viewtype,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
),
)
.await?;
return Ok(true);
}
}
}
@@ -1402,7 +1396,7 @@ impl ChatId {
///
/// `message_timestamp` should be either the message "sent" timestamp or a timestamp of the
/// corresponding event in case of a system message (usually the current system time).
/// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
/// `always_sort_to_bottom` makes this ajust the returned timestamp up so that the message goes
/// to the chat bottom.
/// `received` -- whether the message is received. Otherwise being sent.
/// `incoming` -- whether the message is incoming.
@@ -1990,6 +1984,9 @@ impl Chat {
.ok();
}
// reset encrypt error state eg. for forwarding
msg.param.remove(Param::ErroneousE2ee);
let is_bot = context.get_config_bool(Config::Bot).await?;
msg.param
.set_optional(Param::Bot, Some("1").filter(|_| is_bot));
@@ -2104,19 +2101,13 @@ impl Chat {
} else {
None
};
let new_mime_headers = new_mime_headers.map(|s| new_html_mimepart(s).build().as_string());
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
// We need to add some headers so that they are stripped before formatting HTML by
// `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's
// anyway a useful metadata about the stored text.
true => Some(
"Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text + "\r\n",
),
true => Some(msg.text.clone()),
false => None,
});
let new_mime_headers = match new_mime_headers {
Some(h) => Some(tokio::task::block_in_place(move || {
buf_compress(h.as_bytes())
buf_compress(new_html_mimepart(h).build().as_string().as_bytes())
})?),
None => None,
};
@@ -2216,9 +2207,7 @@ impl Chat {
msg.id = MsgId::new(u32::try_from(raw_id)?);
maybe_set_logging_xdc(context, msg, self.id).await?;
context
.update_webxdc_integration_database(msg, context)
.await?;
context.update_webxdc_integration_database(msg).await?;
}
context.scheduler.interrupt_ephemeral_task().await;
Ok(msg.id)
@@ -2607,12 +2596,10 @@ impl ChatIdBlocked {
_ => (),
}
let protected = contact_id == ContactId::SELF || {
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
peerstate.is_some_and(|p| {
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
})
};
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let protected = peerstate.map_or(false, |p| {
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
});
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
@@ -2677,21 +2664,31 @@ impl ChatIdBlocked {
}
}
/// Prepares a message for sending.
pub async fn prepare_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
ensure!(
!chat_id.is_special(),
"Cannot prepare message for special chat"
);
let msg_id = prepare_msg_common(context, chat_id, msg, MessageState::OutPreparing).await?;
context.emit_msgs_changed(msg.chat_id, msg.id);
Ok(msg_id)
}
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
// the caller should check if the message text is empty
} else if msg.viewtype.has_file() {
let mut blob = msg
.param
.get_blob(Param::File, context)
.get_blob(Param::File, context, !msg.is_increation())
.await?
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
let send_as_is = msg.viewtype == Viewtype::File;
if msg.viewtype == Viewtype::File
|| msg.viewtype == Viewtype::Image
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
{
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
// Correct the type, take care not to correct already very special
// formats as GIF or VOICE.
//
@@ -2700,12 +2697,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
// - from FILE/IMAGE to GIF */
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(&blob.to_abs_path())
{
if msg.viewtype == Viewtype::Sticker {
if better_type != Viewtype::Image {
// UIs don't want conversions of `Sticker` to anything other than `Image`.
msg.param.set_int(Param::ForceSticker, 1);
}
} else if better_type != Viewtype::Webxdc
if better_type != Viewtype::Webxdc
|| context
.ensure_sendable_webxdc_file(&blob.to_abs_path())
.await
@@ -2766,92 +2758,13 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
Ok(())
}
/// Returns whether a contact is in a chat or not.
pub async fn is_contact_in_chat(
context: &Context,
chat_id: ChatId,
contact_id: ContactId,
) -> Result<bool> {
// this function works for group and for normal chats, however, it is more useful
// for group chats.
// ContactId::SELF may be used to check, if the user itself is in a group
// chat (ContactId::SELF is not added to normal chats)
let exists = context
.sql
.exists(
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
(chat_id, contact_id),
)
.await?;
Ok(exists)
}
/// Sends a message object to a chat.
///
/// Sends the event #DC_EVENT_MSGS_CHANGED on success.
/// However, this does not imply, the message really reached the recipient -
/// sending may be delayed eg. due to network problems. However, from your
/// view, you're done with the message. Sooner or later it will find its way.
pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
ensure!(
!chat_id.is_special(),
"chat_id cannot be a special chat: {chat_id}"
);
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
msg.update_param(context).await?;
}
// protect all system messages against RTLO attacks
if msg.is_system_message() {
msg.text = sanitize_bidi_characters(&msg.text);
}
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
if !msg.hidden {
context.emit_msgs_changed(msg.chat_id, msg.id);
}
if msg.param.exists(Param::SetLatitude) {
context.emit_location_changed(Some(ContactId::SELF)).await?;
}
context.scheduler.interrupt_smtp().await;
}
Ok(msg.id)
}
/// Tries to send a message synchronously.
///
/// Creates jobs in the `smtp` table, then drectly opens an SMTP connection and sends the
/// message. If this fails, the jobs remain in the database for later sending.
pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
let rowids = prepare_send_msg(context, chat_id, msg).await?;
if rowids.is_empty() {
return Ok(msg.id);
}
let mut smtp = crate::smtp::Smtp::new();
for rowid in rowids {
send_msg_to_smtp(context, &mut smtp, rowid)
.await
.context("failed to send message, queued for later sending")?;
}
context.emit_msgs_changed(msg.chat_id, msg.id);
Ok(msg.id)
}
/// Prepares a message to be sent out.
///
/// Returns row ids of the `smtp` table.
async fn prepare_send_msg(
async fn prepare_msg_common(
context: &Context,
chat_id: ChatId,
msg: &mut Message,
) -> Result<Vec<i64>> {
change_state_to: MessageState,
) -> Result<MsgId> {
let mut chat = Chat::load_from_db(context, chat_id).await?;
// Check if the chat can be sent to.
@@ -2895,7 +2808,7 @@ async fn prepare_send_msg(
};
// ... then change the MessageState in the message object
msg.state = MessageState::OutPending;
msg.state = change_state_to;
prepare_msg_blob(context, msg).await?;
if !msg.hidden {
@@ -2911,6 +2824,125 @@ async fn prepare_send_msg(
.await?;
msg.chat_id = chat_id;
Ok(msg.id)
}
/// Returns whether a contact is in a chat or not.
pub async fn is_contact_in_chat(
context: &Context,
chat_id: ChatId,
contact_id: ContactId,
) -> Result<bool> {
// this function works for group and for normal chats, however, it is more useful
// for group chats.
// ContactId::SELF may be used to check, if the user itself is in a group
// chat (ContactId::SELF is not added to normal chats)
let exists = context
.sql
.exists(
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
(chat_id, contact_id),
)
.await?;
Ok(exists)
}
/// Sends a message object to a chat.
///
/// Sends the event #DC_EVENT_MSGS_CHANGED on success.
/// However, this does not imply, the message really reached the recipient -
/// sending may be delayed eg. due to network problems. However, from your
/// view, you're done with the message. Sooner or later it will find its way.
// TODO: Do not allow ChatId to be 0, if prepare_msg had been called
// the caller can get it from msg.chat_id. Forwards would need to
// be fixed for this somehow too.
pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
if chat_id.is_unset() {
let forwards = msg.param.get(Param::PrepForwards);
if let Some(forwards) = forwards {
for forward in forwards.split(' ') {
if let Ok(msg_id) = forward.parse::<u32>().map(MsgId::new) {
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
send_msg_inner(context, chat_id, &mut msg).await?;
};
}
}
msg.param.remove(Param::PrepForwards);
msg.update_param(context).await?;
}
return send_msg_inner(context, chat_id, msg).await;
}
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
msg.update_param(context).await?;
}
send_msg_inner(context, chat_id, msg).await
}
/// Tries to send a message synchronously.
///
/// Creates jobs in the `smtp` table, then drectly opens an SMTP connection and sends the
/// message. If this fails, the jobs remain in the database for later sending.
pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
let rowids = prepare_send_msg(context, chat_id, msg).await?;
if rowids.is_empty() {
return Ok(msg.id);
}
let mut smtp = crate::smtp::Smtp::new();
for rowid in rowids {
send_msg_to_smtp(context, &mut smtp, rowid)
.await
.context("failed to send message, queued for later sending")?;
}
context.emit_msgs_changed(msg.chat_id, msg.id);
Ok(msg.id)
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// protect all system messages against RTLO attacks
if msg.is_system_message() {
msg.text = sanitize_bidi_characters(&msg.text);
}
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
if !msg.hidden {
context.emit_msgs_changed(msg.chat_id, msg.id);
}
if msg.param.exists(Param::SetLatitude) {
context.emit_location_changed(Some(ContactId::SELF)).await?;
}
context.scheduler.interrupt_smtp().await;
}
Ok(msg.id)
}
/// Returns row ids of the `smtp` table.
async fn prepare_send_msg(
context: &Context,
chat_id: ChatId,
msg: &mut Message,
) -> Result<Vec<i64>> {
// prepare_msg() leaves the message state to OutPreparing, we
// only have to change the state to OutPending in this case.
// Otherwise we still have to prepare the message, which will set
// the state to OutPending.
if msg.state != MessageState::OutPreparing {
// automatically prepare normal messages
prepare_msg_common(context, chat_id, msg, MessageState::OutPending).await?;
} else {
// update message state of separately prepared messages
ensure!(
chat_id.is_unset() || chat_id == msg.chat_id,
"Inconsistent chat ID"
);
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
}
let row_ids = create_send_msg_jobs(context, msg)
.await
.context("Failed to create send jobs")?;
@@ -2939,8 +2971,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
// because BCC-self messages are also used to detect
// that message was sent if SMTP server is slow to respond
// and connection is frequently lost
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
// disabled by default is fine.
// before receiving status line.
//
// `from` must be the last addr, see `receive_imf_inner()` why.
if context.get_config_bool(Config::BccSelf).await?
@@ -2951,8 +2982,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
recipients.push(from);
}
// Default Webxdc integrations are hidden messages and must not be sent out
if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden {
// Webxdc integrations are messages, however, shipped with main app and must not be sent out
if msg.param.get_int(Param::WebxdcIntegration).is_some() {
recipients.clear();
}
@@ -3240,6 +3271,19 @@ pub async fn get_chat_msgs_ex(
Ok(items)
}
pub(crate) async fn marknoticed_chat_if_older_than(
context: &Context,
chat_id: ChatId,
timestamp: i64,
) -> Result<()> {
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
if timestamp > chat_timestamp {
marknoticed_chat(context, chat_id).await?;
}
}
Ok(())
}
/// Marks all messages in the chat as noticed.
/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
@@ -3251,10 +3295,10 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
.query_map(
"SELECT DISTINCT(m.chat_id) FROM msgs m
LEFT JOIN chats c ON m.chat_id=c.id
WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.archived=1",
(),
WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.blocked=0 AND c.archived=1",
(),
|row| row.get::<_, ChatId>(0),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into)
)
.await?;
if chat_ids_in_archive.is_empty() {
@@ -3263,40 +3307,32 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
context
.sql
.transaction(|transaction| {
let mut stmt = transaction.prepare(
"UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id = ?",
)?;
for chat_id_in_archive in &chat_ids_in_archive {
stmt.execute((chat_id_in_archive,))?;
}
Ok(())
})
.execute(
&format!(
"UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id IN ({});",
sql::repeat_vars(chat_ids_in_archive.len())
),
rusqlite::params_from_iter(&chat_ids_in_archive),
)
.await?;
for chat_id_in_archive in chat_ids_in_archive {
start_chat_ephemeral_timers(context, chat_id_in_archive).await?;
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
}
} else {
start_chat_ephemeral_timers(context, chat_id).await?;
if context
.sql
.execute(
"UPDATE msgs
} else if context
.sql
.execute(
"UPDATE msgs
SET state=?
WHERE state=?
AND hidden=0
AND chat_id=?;",
(MessageState::InNoticed, MessageState::InFresh, chat_id),
)
.await?
== 0
{
return Ok(());
}
(MessageState::InNoticed, MessageState::InFresh, chat_id),
)
.await?
== 0
{
return Ok(());
}
context.emit_event(EventType::MsgsNoticed(chat_id));
@@ -3368,7 +3404,6 @@ pub(crate) async fn mark_old_messages_as_noticed(
}
for c in changed_chats {
start_chat_ephemeral_timers(context, c).await?;
context.emit_event(EventType::MsgsNoticed(c));
chatlist_events::emit_chatlist_item_changed(context, c);
}
@@ -4125,6 +4160,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
bail!("cannot forward drafts.");
}
let original_param = msg.param.clone();
// we tested a sort of broadcast
// by not marking own forwarded messages as such,
// however, this turned out to be to confusing and unclear.
@@ -4147,13 +4184,33 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
// do not leak data as group names; a default subject is generated by mimefactory
msg.subject = "".to_string();
msg.state = MessageState::OutPending;
let new_msg_id = chat
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
.await?;
curr_timestamp += 1;
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
let new_msg_id: MsgId;
if msg.state == MessageState::OutPreparing {
new_msg_id = chat
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
.await?;
curr_timestamp += 1;
msg.param = original_param;
msg.id = src_msg_id;
if let Some(old_fwd) = msg.param.get(Param::PrepForwards) {
let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32());
msg.param.set(Param::PrepForwards, new_fwd);
} else {
msg.param
.set(Param::PrepForwards, new_msg_id.to_u32().to_string());
}
msg.update_param(context).await?;
} else {
msg.state = MessageState::OutPending;
new_msg_id = chat
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
.await?;
curr_timestamp += 1;
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
}
}
created_chats.push(chat_id);
created_msgs.push(new_msg_id);
@@ -4431,7 +4488,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
.await?;
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
// Insert labels for welcome messages to avoid them being readded on reconfiguration.
context
.sql
.execute(
@@ -4593,9 +4650,9 @@ impl Context {
Contact::create_ex(self, Nosync, to, addr).await?;
return Ok(());
}
let addr = ContactAddress::new(addr).context("Invalid address")?;
let (contact_id, _) =
Contact::add_or_lookup(self, "", &addr, Origin::Hidden).await?;
let contact_id = Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None)
.await?
.with_context(|| format!("No contact for addr '{addr}'"))?;
match action {
SyncAction::Block => {
return contact::set_blocked(self, Nosync, contact_id, true).await
@@ -4605,10 +4662,9 @@ impl Context {
}
_ => (),
}
// Use `Request` so that even if the program crashes, the user doesn't have to look
// into the blocked contacts.
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
ChatIdBlocked::lookup_by_contact(self, contact_id)
.await?
.with_context(|| format!("No chat for addr '{addr}'"))?
.id
}
SyncId::Grpid(grpid) => {
@@ -4645,10 +4701,10 @@ impl Context {
/// Emits the appropriate `MsgsChanged` event. Should be called if the number of unnoticed
/// archived chats could decrease. In general we don't want to make an extra db query to know if
/// a noticed chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
/// a noticied chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
/// is ok.
pub(crate) fn on_archived_chats_maybe_noticed(&self) {
self.emit_msgs_changed_without_msg_id(DC_CHAT_ID_ARCHIVED_LINK);
self.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
}
}
@@ -4660,7 +4716,7 @@ mod tests {
use crate::headerdef::HeaderDef;
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{sync, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
use crate::test_utils::{sync, TestContext, TestContextManager};
use strum::IntoEnumIterator;
use tokio::fs;
@@ -4796,12 +4852,15 @@ mod tests {
assert_eq!(test.text, "hello2".to_string());
assert_eq!(test.state, MessageState::OutDraft);
let id_after_prepare = prepare_msg(&t, *chat_id, &mut msg).await?;
assert_eq!(id_after_prepare, id_after_1st_set);
let test = Message::load_from_db(&t, id_after_prepare).await?;
assert_eq!(test.state, MessageState::OutPreparing);
assert!(!test.hidden); // sent draft must no longer be hidden
let id_after_send = send_msg(&t, *chat_id, &mut msg).await?;
assert_eq!(id_after_send, id_after_1st_set);
let test = Message::load_from_db(&t, id_after_send).await?;
assert!(!test.hidden); // sent draft must no longer be hidden
Ok(())
}
@@ -5201,8 +5260,6 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_chat_disordered() -> Result<()> {
let _n = TimeShiftFalsePositiveNote;
// Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy
// (sleep() is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then)
let alice = TestContext::new_alice().await;
@@ -5553,6 +5610,7 @@ mod tests {
let mut msg = Message::new_text("message text".to_string());
assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err());
assert!(prepare_msg(&t, device_chat_id, &mut msg).await.is_err());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
assert!(forward_msgs(&t, &[msg_id], device_chat_id).await.is_err());
@@ -7412,71 +7470,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_accept_before_first_msg() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(alice0).await?;
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
assert_eq!(a0b_contact.origin, Origin::CreateChat);
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not);
sync(alice0, alice1).await;
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::CreateChat);
let a1b_chat = alice1.get_chat(&bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Not);
let chats = Chatlist::try_load(alice1, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
assert_eq!(rcvd_msg.chat_id, a1b_chat.id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_block_before_first_msg() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.block(alice0).await?;
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom);
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes);
sync(alice0, alice1).await;
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::Hidden);
assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id)
.await?
.is_none());
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom);
let a1b_chat = alice1.get_chat(&bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Yes);
assert_eq!(rcvd_msg.chat_id, a1b_chat.id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_adhoc_grp() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
@@ -7703,19 +7696,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_do_not_overwrite_draft() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let mut msg = Message::new_text("This is a draft message".to_string());
let self_chat = alice.get_self_chat().await.id;
self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap();
let draft1 = self_chat.get_draft(&alice).await?.unwrap();
SystemTime::shift(Duration::from_secs(1));
self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap();
let draft2 = self_chat.get_draft(&alice).await?.unwrap();
assert_eq!(draft1.timestamp_sort, draft2.timestamp_sort);
Ok(())
}
}

View File

@@ -394,32 +394,25 @@ impl Chatlist {
&chat_loaded
};
let lastmsg = if let Some(lastmsg_id) = lastmsg_id {
// Message may be deleted by the time we try to load it,
// so use `load_from_db_optional` instead of `load_from_db`.
Message::load_from_db_optional(context, lastmsg_id)
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id)
.await
.context("Loading message failed")?
} else {
None
};
let lastcontact = if let Some(lastmsg) = &lastmsg {
.context("loading message failed")?;
if lastmsg.from_id == ContactId::SELF {
None
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
Some(lastcontact)
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single => None,
Chattype::Single => (Some(lastmsg), None),
}
}
} else {
None
(None, None)
};
if chat.id.is_archived_link() {
@@ -768,25 +761,6 @@ mod tests {
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
}
/// Tests that summary does not fail to load
/// if the draft was deleted after loading the chatlist.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_deleted_draft() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let mut msg = Message::new_text("Foobar".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
chat_id.set_draft(&t, None).await.unwrap();
let summary_res = chats.get_summary(&t, 0, None).await;
assert!(summary_res.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;

View File

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

View File

@@ -36,10 +36,10 @@ use crate::message::Message;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::time;
use crate::{chat, e2ee, provider};
use crate::{stock_str, EventType};
use deltachat_contact_tools::addr_cmp;
macro_rules! progress {
@@ -452,9 +452,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
let create = true;
imap_session
.select_with_uidvalidity(ctx, "INBOX", create)
.select_with_uidvalidity(ctx, "INBOX")
.await
.context("could not read INBOX status")?;
@@ -487,7 +486,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
update_device_chats_handle.await??;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.emit_event(EventType::AccountsItemChanged);
Ok(configured_param)
}
@@ -613,6 +611,8 @@ pub enum Error {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::login_param::EnteredServerLoginParam;

View File

@@ -67,15 +67,19 @@ fn parse_server<B: BufRead>(
let typ = server_event
.attributes()
.find_map(|attr| {
attr.ok().filter(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.eq_ignore_ascii_case("type")
})
.find(|attr| {
attr.as_ref()
.map(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.to_lowercase()
== "type"
})
.unwrap_or_default()
})
.map(|typ| {
typ.decode_and_unescape_value(reader.decoder())
typ.unwrap()
.decode_and_unescape_value(reader.decoder())
.unwrap_or_default()
.to_lowercase()
})
@@ -268,6 +272,8 @@ pub(crate) async fn moz_autoconfigure(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]

View File

@@ -215,6 +215,8 @@ pub(crate) async fn outlk_autodiscover(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]

View File

@@ -129,19 +129,22 @@ impl ContactId {
) -> Result<()> {
context
.sql
.transaction(|transaction| {
let mut stmt = transaction
.prepare("UPDATE contacts SET origin=?1 WHERE id = ?2 AND origin < ?1")?;
for id in ids {
stmt.execute((origin, id))?;
}
Ok(())
})
.execute(
&format!(
"UPDATE contacts SET origin=? WHERE id IN ({}) AND origin<?",
sql::repeat_vars(ids.len())
),
rusqlite::params_from_iter(
params_iter(&[origin])
.chain(params_iter(ids))
.chain(params_iter(&[origin])),
),
)
.await?;
Ok(())
}
/// Returns contact address.
/// Returns contact adress.
pub async fn addr(&self, context: &Context) -> Result<String> {
let addr = context
.sql
@@ -1249,15 +1252,15 @@ impl Contact {
let fingerprint_self = load_self_public_key(context)
.await?
.dc_fingerprint()
.fingerprint()
.to_string();
let fingerprint_other_verified = peerstate
.peek_key(true)
.map(|k| k.dc_fingerprint().to_string())
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(false)
.map(|k| k.dc_fingerprint().to_string())
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
if addr < peerstate.addr {
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
@@ -1981,7 +1984,7 @@ mod tests {
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
use crate::test_utils::{self, TestContext, TestContextManager};
#[test]
fn test_contact_id_values() {
@@ -2912,8 +2915,6 @@ Hi."#;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_was_seen_recently() -> Result<()> {
let _n = TimeShiftFalsePositiveNote;
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -2929,7 +2930,18 @@ Hi."#;
bob.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
assert!(contact.was_seen_recently());
let green = nu_ansi_term::Color::Green.normal();
assert!(
contact.was_seen_recently(),
"{}",
green.paint(
"\nNOTE: This test failure is probably a false-positive, caused by tests running in parallel.
The issue is that `SystemTime::shift()` (a utility function for tests) changes the time for all threads doing tests, and not only for the running test.
Until the false-positive is fixed:
- Use `cargo test -- --test-threads 1` instead of `cargo test`
- Or use `cargo nextest run` (install with `cargo install cargo-nextest --locked`)\n"
)
);
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
assert!(!self_contact.was_seen_recently());
@@ -3232,20 +3244,4 @@ Hi."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_is_verified() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
assert_eq!(contact.is_verified(&alice).await?, true);
assert!(contact.is_profile_verified(&alice).await?);
assert!(contact.get_verifier_id(&alice).await?.is_none());
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
Ok(())
}
}

View File

@@ -10,10 +10,9 @@ use std::time::Duration;
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use pgp::types::PublicKeyTrait;
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use tokio::sync::{Mutex, Notify, OnceCell, RwLock};
use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
@@ -276,7 +275,7 @@ pub struct InnerContext {
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: parking_lot::RwLock<String>,
pub(crate) last_error: std::sync::RwLock<String>,
/// If debug logging is enabled, this contains all necessary information
///
@@ -292,7 +291,7 @@ pub struct InnerContext {
pub(crate) push_subscribed: AtomicBool,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
pub(crate) iroh: OnceCell<Iroh>,
}
/// The state of ongoing process.
@@ -446,11 +445,11 @@ impl Context {
metadata: RwLock::new(None),
creation_time: tools::Time::now(),
last_full_folder_scan: Mutex::new(None),
last_error: parking_lot::RwLock::new("".to_string()),
last_error: std::sync::RwLock::new("".to_string()),
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
iroh: Arc::new(RwLock::new(None)),
iroh: OnceCell::new(),
};
let ctx = Context {
@@ -472,33 +471,12 @@ impl Context {
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
// The next line is mainly for iOS:
// iOS starts a separate process for receiving notifications and if the user concurrently
// starts the app, the UI process opens the database but waits with calling start_io()
// until the notifications process finishes.
// Now, some configs may have changed, so, we need to invalidate the cache.
self.sql.config_cache.write().await.clear();
self.scheduler.start(self.clone()).await;
}
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
self.scheduler.stop(self).await;
if let Some(iroh) = self.iroh.write().await.take() {
// Close all QUIC connections.
// Spawn into a separate task,
// because Iroh calls `wait_idle()` internally
// and it may take time, especially if the network
// has become unavailable.
tokio::spawn(async move {
// We do not log the error because we do not want the task
// to hold the reference to Context.
let _ = tokio::time::timeout(Duration::from_secs(60), iroh.close()).await;
});
}
}
/// Restarts the IO scheduler if it was running before
@@ -509,7 +487,7 @@ impl Context {
/// Indicate that the network likely has come back.
pub async fn maybe_network(&self) {
if let Some(ref iroh) = *self.iroh.read().await {
if let Some(iroh) = self.iroh.get() {
iroh.network_change().await;
}
self.scheduler.maybe_network().await;
@@ -553,7 +531,23 @@ impl Context {
if self.scheduler.is_running().await {
self.scheduler.maybe_network().await;
self.wait_for_all_work_done().await;
// Wait until fetching is finished.
// Ideally we could wait for connectivity change events,
// but sleep loop is good enough.
// First 100 ms sleep in chunks of 10 ms.
for _ in 0..10 {
if self.all_work_done().await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
// If we are not finished in 100 ms, keep waking up every 100 ms.
while !self.all_work_done().await {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
} else {
// Pause the scheduler to ensure another connection does not start
// while we are fetching on a dedicated connection.
@@ -643,36 +637,14 @@ impl Context {
}
/// Emits a MsgsChanged event with specified chat and message ids
///
/// If IDs are unset, [`Self::emit_msgs_changed_without_ids`]
/// or [`Self::emit_msgs_changed_without_msg_id`] should be used
/// instead of this function.
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
debug_assert!(!chat_id.is_unset());
debug_assert!(!msg_id.is_unset());
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
chatlist_events::emit_chatlist_item_changed(self, chat_id);
}
/// Emits a MsgsChanged event with specified chat and without message id.
pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
debug_assert!(!chat_id.is_unset());
self.emit_event(EventType::MsgsChanged {
chat_id,
msg_id: MsgId::new(0),
});
chatlist_events::emit_chatlist_changed(self);
chatlist_events::emit_chatlist_item_changed(self, chat_id);
}
/// Emits an IncomingMsg event with specified chat and message ids
pub fn emit_incoming_msg(&self, chat_id: ChatId, msg_id: MsgId) {
debug_assert!(!chat_id.is_unset());
debug_assert!(!msg_id.is_unset());
self.emit_event(EventType::IncomingMsg { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
chatlist_events::emit_chatlist_item_changed(self, chat_id);
@@ -809,7 +781,7 @@ impl Context {
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
let fingerprint_str = match load_self_public_key(self).await {
Ok(key) => key.dc_fingerprint().hex(),
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {err}>"),
};
@@ -1205,7 +1177,7 @@ impl Context {
EncryptPreference::Mutual,
&public_key,
);
let fingerprint = public_key.dc_fingerprint();
let fingerprint = public_key.fingerprint();
peerstate.set_verified(public_key, fingerprint, "".to_string())?;
peerstate.save_to_db(&self.sql).await?;
chat_id
@@ -1777,7 +1749,6 @@ mod tests {
"socks5_password",
"key_id",
"webxdc_integration",
"device_token",
];
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();
@@ -2092,41 +2063,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
let alice = TestContext::new_alice().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Change the config circumventing the cache
// This simulates what the notification plugin on iOS might do
// because it runs in a different process
alice
.sql
.execute(
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
(),
)
.await?;
// Alice's Delta Chat doesn't know about it yet:
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Starting IO will fail of course because no server settings are configured,
// but it should invalidate the caches:
alice.start_io().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("0".to_string())
);
Ok(())
}
}

View File

@@ -60,11 +60,9 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
"time": time,
}),
info: None,
href: None,
summary: None,
document: None,
uid: None,
notify: None,
},
time,
)

View File

@@ -182,7 +182,7 @@ pub(crate) async fn get_autocrypt_peerstate(
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.dc_fingerprint(),
&header.public_key.fingerprint(),
from,
)
.await?;

View File

@@ -3,7 +3,7 @@
use std::cmp::max;
use std::collections::BTreeMap;
use anyhow::{anyhow, bail, ensure, Result};
use anyhow::{anyhow, bail, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
@@ -201,11 +201,7 @@ impl Session {
bail!("Attempt to fetch UID 0");
}
let create = false;
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
.await?;
ensure!(folder_exists, "No folder {folder}");
self.select_with_uidvalidity(context, folder).await?;
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);
@@ -444,7 +440,7 @@ mod tests {
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
alice
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#)
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#, "d")
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;

View File

@@ -303,12 +303,12 @@ Sent with my Delta Chat Messenger: https://delta.chat";
last_seen_autocrypt: 14,
prefer_encrypt,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 15,
gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,

View File

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

View File

@@ -59,7 +59,7 @@ pub enum EventType {
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a message box then.
/// in a messasge box then.
Error(String),
/// An action cannot be performed because the user is not in the group.
@@ -107,24 +107,6 @@ pub enum EventType {
reaction: Reaction,
},
/// A webxdc wants an info message or a changed summary to be notified.
IncomingWebxdcNotify {
/// ID of the chat.
chat_id: ChatId,
/// ID of the contact sending.
contact_id: ContactId,
/// ID of the added info message or webxdc instance in case of summary change.
msg_id: MsgId,
/// Text to notify.
text: String,
/// Link assigned to this notification, if any.
href: Option<String>,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -350,20 +332,6 @@ pub enum EventType {
chat_id: Option<ChatId>,
},
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
///
/// This event is only emitted by the account manager
AccountsChanged,
/// Inform that an account property that might be shown in the account list changed, namely:
/// - is_configured (see [crate::context::Context::is_configured])
/// - displayname
/// - selfavatar
/// - private_tag
///
/// This event is emitted from the account whose property changed.
AccountsItemChanged,
/// Event for using in tests, e.g. as a fence between normally generated events.
#[cfg(test)]
Test,

View File

@@ -73,7 +73,6 @@ pub enum HeaderDef {
/// [Autocrypt](https://autocrypt.org/) header.
Autocrypt,
AutocryptGossip,
AutocryptSetupMessage,
SecureJoin,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use ::pgp::types::PublicKeyTrait;
use ::pgp::types::KeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use futures::TryStreamExt;
use futures_lite::FutureExt;
@@ -416,7 +416,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
.context("cannot import unpacked database");
}
if res.is_ok() {
res = adjust_bcc_self(context).await;
res = adjust_delete_server_after(context).await;
}
fs::remove_file(unpacked_database)
.await
@@ -426,7 +426,6 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
if res.is_ok() {
context.emit_event(EventType::ImexProgress(999));
res = context.sql.run_migrations(context).await;
context.emit_event(EventType::AccountsItemChanged);
}
if res.is_ok() {
delete_and_reset_all_device_msgs(context)
@@ -751,7 +750,7 @@ where
true => "private",
};
let id = id.map_or("default".into(), |i| i.to_string());
let fp = key.dc_fingerprint().hex();
let fp = DcKey::fingerprint(key).hex();
format!("{kind}-key-{addr}-{id}-{fp}.asc")
};
let path = dir.join(&file_name);
@@ -796,7 +795,7 @@ async fn export_database(
.to_str()
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
adjust_bcc_self(context).await?;
adjust_delete_server_after(context).await?;
context
.sql
.set_raw_config_int("backup_time", timestamp)
@@ -826,14 +825,15 @@ async fn export_database(
.await
}
/// Sets `Config::BccSelf` (and `DeleteServerAfter` to "never" in effect) if needed so that new
/// messages are present on the server after a backup restoration or available for all devices in
/// multi-device case. NB: Calling this after a backup import isn't reliable as we can crash in
/// between, but this is a problem only for old backups, new backups already have `BccSelf` set if
/// necessary.
async fn adjust_bcc_self(context: &Context) -> Result<()> {
if context.is_chatmail().await? && !context.config_exists(Config::BccSelf).await? {
context.set_config(Config::BccSelf, Some("1")).await?;
/// Sets `Config::DeleteServerAfter` to "never" if needed so that new messages are present on the
/// server after a backup restoration or available for all devices in multi-device case.
/// NB: Calling this after a backup import isn't reliable as we can crash in between, but this is a
/// problem only for old backups, new backups already have `DeleteServerAfter` set if necessary.
async fn adjust_delete_server_after(context: &Context) -> Result<()> {
if context.is_chatmail().await? && !context.config_exists(Config::DeleteServerAfter).await? {
context
.set_config(Config::DeleteServerAfter, Some("0"))
.await?;
}
Ok(())
}
@@ -878,7 +878,7 @@ mod tests {
.unwrap()
.strip_suffix(".asc")
.unwrap();
assert_eq!(fingerprint, key.dc_fingerprint().hex());
assert_eq!(fingerprint, DcKey::fingerprint(&key).hex());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{blobdir}/{filename}");
let bytes = tokio::fs::read(&filename).await.unwrap();
@@ -1029,20 +1029,12 @@ mod tests {
let context1 = &TestContext::new_alice().await;
// Check that the settings are displayed correctly.
assert_eq!(
context1.get_config(Config::BccSelf).await?,
Some("1".to_string())
);
// Check that the setting is displayed correctly.
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
context1.set_config_bool(Config::IsChatmail, true).await?;
assert_eq!(
context1.get_config(Config::BccSelf).await?,
Some("0".to_string())
);
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("1".to_string())
@@ -1065,10 +1057,6 @@ mod tests {
assert!(context2.is_configured().await?);
assert!(context2.is_chatmail().await?);
for ctx in [context1, context2] {
assert_eq!(
ctx.get_config(Config::BccSelf).await?,
Some("1".to_string())
);
assert_eq!(
ctx.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())

View File

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

View File

@@ -11,8 +11,7 @@ use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{PublicKeyTrait, SecretKeyTrait};
use rand::thread_rng;
use pgp::types::{KeyTrait, SecretKeyTrait};
use tokio::runtime::Handle;
use crate::config::Config;
@@ -27,42 +26,10 @@ use crate::tools::{self, time_elapsed};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
if let Ok(res) = res {
return Ok(res);
}
// Workaround for keys imported using
// Delta Chat core < 1.0.0.
// Old Delta Chat core had a bug
// that resulted in treating CRC24 checksum
// as part of the key when reading ASCII Armor.
// Some users that started using Delta Chat in 2019
// have such corrupted keys with garbage bytes at the end.
//
// Garbage is at least 3 bytes long
// and may be longer due to padding
// at the end of the real key data
// and importing the key multiple times.
//
// If removing 10 bytes is not enough,
// the key is likely actually corrupted.
for garbage_bytes in 3..std::cmp::min(bytes.len(), 10) {
let res = <Self as Deserializable>::from_bytes(Cursor::new(
bytes
.get(..bytes.len().saturating_sub(garbage_bytes))
.unwrap_or_default(),
));
if let Ok(res) = res {
return Ok(res);
}
}
// Removing garbage bytes did not help, return the error.
Ok(res?)
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
}
/// Create a key from a base64 string.
@@ -126,8 +93,8 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
/// The fingerprint for the key.
fn dc_fingerprint(&self) -> Fingerprint {
PublicKeyTrait::fingerprint(self).into()
fn fingerprint(&self) -> Fingerprint {
Fingerprint::new(KeyTrait::fingerprint(self))
}
fn is_private() -> bool;
@@ -266,8 +233,7 @@ impl DcSecretKey for SignedSecretKey {
fn split_public_key(&self) -> Result<SignedPublicKey> {
self.verify()?;
let unsigned_pubkey = SecretKeyTrait::public_key(self);
let mut rng = thread_rng();
let signed_pubkey = unsigned_pubkey.sign(&mut rng, self, || "".into())?;
let signed_pubkey = unsigned_pubkey.sign(self, || "".into())?;
Ok(signed_pubkey)
}
}
@@ -429,12 +395,6 @@ impl Fingerprint {
}
}
impl From<pgp::types::Fingerprint> for Fingerprint {
fn from(fingerprint: pgp::types::Fingerprint) -> Fingerprint {
Self::new(fingerprint.as_bytes().into())
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")
@@ -597,36 +557,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
}
/// Tests workaround for Delta Chat core < 1.0.0
/// which parsed CRC24 at the end of ASCII Armor
/// as the part of the key.
/// Depending on the alignment and the number of
/// `=` characters at the end of the key,
/// this resulted in various number of garbage
/// octets at the end of the key, starting from 3 octets,
/// but possibly 4 or 5 and maybe more octets
/// if the key is imported or transferred
/// using Autocrypt Setup Message multiple times.
#[test]
fn test_ignore_trailing_garbage() {
// Test several variants of garbage.
for garbage in [
b"\x02\xfc\xaa\x38\x4b\x5c".as_slice(),
b"\x02\xfc\xaa".as_slice(),
b"\x01\x02\x03\x04\x05".as_slice(),
] {
let private_key = KEYPAIR.secret.clone();
let mut binary = DcKey::to_bytes(&private_key);
binary.extend(garbage);
let private_key2 =
SignedSecretKey::from_slice(&binary).expect("Failed to ignore garbage");
assert_eq!(private_key.dc_fingerprint(), private_key2.dc_fingerprint());
}
}
#[test]
fn test_base64_roundtrip() {
let key = KEYPAIR.public.clone();

View File

@@ -16,8 +16,7 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,

View File

@@ -244,14 +244,18 @@ impl Kml {
self.tag = KmlTag::PlacemarkPoint;
} else if tag == "coordinates" && self.tag == KmlTag::PlacemarkPoint {
self.tag = KmlTag::PlacemarkPointCoordinates;
if let Some(acc) = event.attributes().find_map(|attr| {
attr.ok().filter(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.eq_ignore_ascii_case("accuracy")
})
if let Some(acc) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.to_lowercase()
== "accuracy"
})
.unwrap_or_default()
}) {
let v = acc
.unwrap()
.decode_and_unescape_value(reader.decoder())
.unwrap_or_default();
@@ -707,6 +711,9 @@ pub(crate) async fn save(
))?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
drop(stmt_test);
drop(stmt_insert);
newest_timestamp = timestamp;
newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
}
@@ -873,6 +880,8 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::message::MessageState;
@@ -1127,10 +1136,6 @@ Content-Disposition: attachment; filename="location.kml"
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
// Location-only messages are "auto-generated", but they mustn't make the contact a bot.
let contact = bob.add_or_lookup_contact(alice).await;
assert!(!contact.is_bot());
// Day later Bob removes location.
SystemTime::shift(Duration::from_secs(86400));
delete_expired(alice, time()).await?;

View File

@@ -50,13 +50,13 @@ impl Context {
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
pub fn set_last_error(&self, error: &str) {
let mut last_error = self.last_error.write();
let mut last_error = self.last_error.write().unwrap();
*last_error = error.to_string();
}
/// Get last error string.
pub fn get_last_error(&self) -> String {
let last_error = &*self.last_error.read();
let last_error = &*self.last_error.read().unwrap();
last_error.clone()
}
}

View File

@@ -293,7 +293,13 @@ impl MsgId {
ret += ", Location sent";
}
if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
let e2ee_errors = msg.param.get_int(Param::ErroneousE2ee).unwrap_or_default();
if 0 != e2ee_errors {
if 0 != e2ee_errors & 0x2 {
ret += ", Encrypted, no valid signature";
}
} else if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
ret += ", Encrypted";
}
@@ -342,7 +348,7 @@ impl MsgId {
let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
for server_url in server_urls {
// Format as RFC 5092 relative IMAP URL.
ret += &format!("\nServer-URL: {server_url}");
ret += &format!("\n{server_url}");
}
}
let hop_info = self.hop_info(context).await?;
@@ -718,7 +724,7 @@ impl Message {
/// `contact_id` set to [`ContactId::SELF`].
///
/// `latitude` is the North-south position of the location.
/// `longitude` is the East-west position of the location.
/// `longitutde` is the East-west position of the location.
///
/// [`location::set()`]: crate::location::set
/// [`send_locations_to_chat()`]: crate::location::send_locations_to_chat
@@ -947,6 +953,18 @@ impl Message {
cmd != SystemMessage::Unknown
}
/// Whether the message is still being created.
///
/// Messages with attachments might be created before the
/// attachment is ready. In this case some more restrictions on
/// the attachment apply, e.g. if the file to be attached is still
/// being written to or otherwise will still change it can not be
/// copied to the blobdir. Thus those attachments need to be
/// created immediately in the blobdir with a valid filename.
pub fn is_increation(&self) -> bool {
self.viewtype.has_file() && self.state == MessageState::OutPreparing
}
/// Returns true if the message is an Autocrypt Setup Message.
pub fn is_setupmessage(&self) -> bool {
if self.viewtype != Viewtype::File {
@@ -1607,15 +1625,15 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
modified_chat_ids.insert(msg.chat_id);
let target = context.get_delete_msgs_target().await?;
let update_db = |trans: &mut rusqlite::Transaction| {
trans.execute(
let update_db = |conn: &mut rusqlite::Connection| {
conn.execute(
"UPDATE imap SET target=? WHERE rfc724_mid=?",
(target, msg.rfc724_mid),
)?;
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
conn.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
Ok(())
};
if let Err(e) = context.sql.transaction(update_db).await {
if let Err(e) = context.sql.call_write(update_db).await {
error!(context, "delete_msgs: failed to update db: {e:#}.");
res = Err(e);
continue;
@@ -1637,7 +1655,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
res?;
for modified_chat_id in modified_chat_ids {
context.emit_msgs_changed_without_msg_id(modified_chat_id);
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
}
@@ -1675,7 +1693,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
m.id AS id,
m.chat_id AS chat_id,
m.state AS state,
m.download_state as download_state,
m.ephemeral_timer AS ephemeral_timer,
m.param AS param,
m.from_id AS from_id,
@@ -1691,7 +1708,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let state: MessageState = row.get("state")?;
let download_state: DownloadState = row.get("download_state")?;
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
@@ -1703,7 +1719,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
id,
chat_id,
state,
download_state,
param,
from_id,
rfc724_mid,
@@ -1733,7 +1748,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
id,
curr_chat_id,
curr_state,
curr_download_state,
curr_param,
curr_from_id,
curr_rfc724_mid,
@@ -1743,14 +1757,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
_curr_ephemeral_timer,
) in msgs
{
if curr_download_state != DownloadState::Done {
if curr_state == MessageState::InFresh {
// Don't mark partially downloaded messages as seen or send a read receipt since
// they are not really seen by the user.
update_msg_state(context, id, MessageState::InNoticed).await?;
updated_chat_ids.insert(curr_chat_id);
}
} else if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
update_msg_state(context, id, MessageState::InSeen).await?;
info!(context, "Seen message {}.", id);
@@ -2088,9 +2095,6 @@ pub enum Viewtype {
Gif = 21,
/// Message containing a sticker, similar to image.
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
///
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker = 23,
@@ -2191,6 +2195,38 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prepare_message_and_send() {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"))
.await
.unwrap();
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
let msg_id = chat::prepare_msg(ctx, chat.id, &mut msg).await.unwrap();
let _msg2 = Message::load_from_db(ctx, msg_id).await.unwrap();
assert_eq!(_msg2.get_filemime(), None);
}
/// Tests that message can be prepared even if account has no configured address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prepare_not_configured() {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_webrtc_instance() {
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
@@ -2310,9 +2346,9 @@ mod tests {
let mut msg = Message::new_text("Quoted message".to_string());
// Send message, so it gets a Message-Id.
// Prepare message for sending, so it gets a Message-Id.
assert!(msg.rfc724_mid.is_empty());
let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap();
let msg_id = chat::prepare_msg(ctx, chat.id, &mut msg).await.unwrap();
let msg = Message::load_from_db(ctx, msg_id).await.unwrap();
assert!(!msg.rfc724_mid.is_empty());
@@ -2547,110 +2583,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_markseen_not_downloaded_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
.await?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert_eq!(msg.state, MessageState::InFresh);
markseen_msgs(alice, vec![msg.id]).await?;
// A not downloaded message can be seen only if it's seen on another device.
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
// Marking the message as seen again is a no op.
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::InProgress)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::Failure)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::Undecipherable)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
assert!(
!alice
.sql
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
.await?
);
alice.set_config(Config::DownloadLimit, None).await?;
// Let's assume that Alice and Bob resolved the problem with encryption.
let old_msg = msg;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, old_msg.chat_id);
assert_eq!(msg.download_state, DownloadState::Done);
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert!(msg.get_showpadlock());
// The message state mustn't be downgraded to `InFresh`.
assert_eq!(msg.state, MessageState::InNoticed);
markseen_msgs(alice, vec![msg.id]).await?;
let msg = Message::load_from_db(alice, msg.id).await?;
assert_eq!(msg.state, MessageState::InSeen);
assert_eq!(
alice
.sql
.count("SELECT COUNT(*) FROM smtp_mdns", ())
.await?,
1
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
.await?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(msg.state, MessageState::InFresh);
alice.set_config(Config::DownloadLimit, None).await?;
let seen = true;
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
.await
.unwrap()
.unwrap();
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
.await
.unwrap();
assert_eq!(msg.download_state, DownloadState::Done);
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert!(msg.get_showpadlock());
assert_eq!(msg.state, MessageState::InSeen);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_state() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -2707,29 +2639,6 @@ mod tests {
async fn test_is_bot() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice receives an auto-generated non-chat message.
//
// This could be a holiday notice,
// in which case the message should be marked as bot-generated,
// but the contact should not.
receive_imf(
&alice,
b"From: Claire <claire@example.com>\n\
To: alice@example.org\n\
Message-ID: <789@example.com>\n\
Auto-Submitted: auto-generated\n\
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello\n",
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text(), "hello".to_string());
assert!(msg.is_bot());
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
assert!(!contact.is_bot());
// Alice receives a message from Bob the bot.
receive_imf(
&alice,

View File

@@ -6,6 +6,7 @@ use anyhow::{bail, Context as _, Result};
use base64::Engine as _;
use chrono::TimeZone;
use email::Mailbox;
use format_flowed::{format_flowed, format_flowed_quote};
use lettre_email::{Address, Header, MimeMultipartType, PartBuilder};
use tokio::fs;
@@ -356,7 +357,7 @@ impl MimeFactory {
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
// Otherwise "smeared timestamps" may result in the condition
// Othewise "smeared timestamps" may result in the condition
// to fail even if the clock is monotonic.
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
Ok(true)
@@ -1299,18 +1300,9 @@ impl MimeFactory {
let final_text = placeholdertext.as_deref().unwrap_or(&msg.text);
let mut quoted_text = None;
if let Some(msg_quoted_text) = msg.quoted_text() {
let mut some_quoted_text = String::new();
for quoted_line in msg_quoted_text.split('\n') {
some_quoted_text += "> ";
some_quoted_text += quoted_line;
some_quoted_text += "\r\n";
}
some_quoted_text += "\r\n";
quoted_text = Some(some_quoted_text)
}
let mut quoted_text = msg
.quoted_text()
.map(|quote| format_flowed_quote(&quote) + "\r\n\r\n");
if !is_encrypted && msg.param.get_bool(Param::ProtectQuote).unwrap_or_default() {
// Message is not encrypted but quotes encrypted message.
quoted_text = Some("> ...\r\n\r\n".to_string());
@@ -1320,6 +1312,7 @@ impl MimeFactory {
// Delta Chat.
quoted_text = Some("\r\n".to_string());
}
let flowed_text = format_flowed(final_text);
let is_reaction = msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
@@ -1329,7 +1322,7 @@ impl MimeFactory {
"{}{}{}{}{}{}",
fwdhint.unwrap_or_default(),
quoted_text.unwrap_or_default(),
escape_message_footer_marks(final_text),
escape_message_footer_marks(&flowed_text),
if !final_text.is_empty() && !footer.is_empty() {
"\r\n\r\n"
} else {
@@ -1339,8 +1332,10 @@ impl MimeFactory {
footer
);
let mut main_part =
PartBuilder::new().header(("Content-Type", "text/plain; charset=utf-8"));
let mut main_part = PartBuilder::new().header((
"Content-Type",
"text/plain; charset=utf-8; format=flowed; delsp=no",
));
main_part = self.add_message_text(main_part, message_text);
if is_reaction {
@@ -1369,7 +1364,7 @@ impl MimeFactory {
// add attachment part
if msg.viewtype.has_file() {
let file_part = build_body_file(context, &msg).await?;
let (file_part, _) = build_body_file(context, &msg, "").await?;
parts.push(file_part);
}
@@ -1509,10 +1504,14 @@ pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
.join("\r\n")
}
async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder> {
async fn build_body_file(
context: &Context,
msg: &Message,
base_name: &str,
) -> Result<(PartBuilder, String)> {
let blob = msg
.param
.get_blob(Param::File, context)
.get_blob(Param::File, context, true)
.await?
.context("msg has no file")?;
let suffix = blob.suffix().unwrap_or("dat");
@@ -1535,13 +1534,17 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder
),
Viewtype::Image | Viewtype::Gif => format!(
"image_{}.{}",
chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0)
.single()
.map_or_else(
|| "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
),
if base_name.is_empty() {
chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0)
.single()
.map_or_else(
|| "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
)
} else {
base_name.to_string()
},
&suffix,
),
Viewtype::Video => format!(
@@ -1593,7 +1596,7 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder
.header(("Content-Transfer-Encoding", "base64"))
.body(encoded_body);
Ok(mail)
Ok((mail, filename_to_send))
}
async fn build_avatar_file(context: &Context, path: &str) -> Result<String> {
@@ -1897,7 +1900,7 @@ mod tests {
)
.await
.unwrap();
let mut new_msg = incoming_msg_to_reply_msg(
let new_msg = incoming_msg_to_reply_msg(
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: bob@example.com\n\
To: alice@example.org\n\
@@ -1923,9 +1926,6 @@ mod tests {
Original-Message-ID: <2893@example.com>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n", &t).await;
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
.await
.unwrap();
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
// The subject string should not be "Re: message opened"
assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap());
@@ -2072,7 +2072,7 @@ mod tests {
let mut new_msg = Message::new_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap();
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
@@ -2129,7 +2129,7 @@ mod tests {
) -> String {
let t = TestContext::new_alice().await;
let mut new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await;
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 1).await;
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 2).await;
if delete_original_msg {
incoming_msg.id.trash(&t, false).await.unwrap();
@@ -2159,9 +2159,6 @@ mod tests {
new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap();
}
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
.await
.unwrap();
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
mf.subject_str(&t).await.unwrap()
}
@@ -2182,6 +2179,9 @@ mod tests {
let mut new_msg = Message::new_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::prepare_msg(context, chat_id, &mut new_msg)
.await
.unwrap();
new_msg
}
@@ -2192,7 +2192,7 @@ mod tests {
let t = TestContext::new_alice().await;
let context = &t;
let mut msg = incoming_msg_to_reply_msg(
let msg = incoming_msg_to_reply_msg(
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: Charlie <charlie@example.com>\n\
To: alice@example.org\n\
@@ -2205,7 +2205,6 @@ mod tests {
context,
)
.await;
chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap();
let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap();
@@ -2275,6 +2274,7 @@ mod tests {
let msg = message.as_string();
let header_end = msg.find("Hi").unwrap();
#[allow(clippy::indexing_slicing)]
let headers = msg[0..header_end].trim();
assert!(!headers.lines().any(|l| l.trim().is_empty()));

View File

@@ -17,10 +17,10 @@ use rand::distributions::{Alphanumeric, DistString};
use crate::aheader::{Aheader, EncryptPreference};
use crate::authres::handle_authres;
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::chat::{add_info_msg, ChatId};
use crate::config::Config;
use crate::constants;
use crate::contact::ContactId;
use crate::constants::{self, Chattype};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::decrypt::{
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
@@ -36,7 +36,8 @@ use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
use crate::sync::SyncItems;
use crate::tools::{
get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text, validate_id,
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text,
validate_id,
};
use crate::{chatlist_events, location, stock_str, tools};
@@ -105,26 +106,20 @@ pub(crate) struct MimeMessage {
/// received.
pub(crate) footer: Option<String>,
/// If set, this is a modified MIME message; clients should offer a way to view the original
/// MIME message in this case.
// if this flag is set, the parts/text/etc. are just close to the original mime-message;
// clients should offer a way to view the original message in this case
pub is_mime_modified: bool,
/// Decrypted, raw MIME structure. Nonempty iff `is_mime_modified` and the message was actually
/// encrypted.
/// The decrypted, raw mime structure.
///
/// This is non-empty iff `is_mime_modified` and the message was actually encrypted. It is used
/// for e.g. late-parsing HTML.
pub decoded_data: Vec<u8>,
/// Hop info for debugging.
pub(crate) hop_info: String,
/// Whether the message is auto-generated.
///
/// If chat message (with `Chat-Version` header) is auto-generated,
/// the contact sending this should be marked as bot.
///
/// If non-chat message is auto-generated,
/// it could be a holiday notice auto-reply,
/// in which case the message should be marked as bot-generated,
/// but the contact should not be.
/// Whether the contact sending this should be marked as bot or non-bot.
pub(crate) is_bot: Option<bool>,
/// When the message was received, in secs since epoch.
@@ -570,14 +565,11 @@ impl MimeMessage {
},
};
let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
if parser.mdn_reports.is_empty()
&& !is_location_only
&& parser.sync_items.is_none()
&& parser.webxdc_status_update.is_none()
{
let is_bot =
parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
if parser.mdn_reports.is_empty() && parser.webxdc_status_update.is_none() {
// "Auto-Submitted" is also set by holiday-notices so we also check "chat-version".
let is_bot = parser.headers.get("auto-submitted")
== Some(&"auto-generated".to_string())
&& parser.headers.contains_key("chat-version");
parser.is_bot = Some(is_bot);
}
parser.maybe_remove_bad_parts();
@@ -673,13 +665,11 @@ impl MimeMessage {
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
/// containing a description. If such a message is detected, text from the first part can be
/// moved to the second part, and the first part dropped.
#[allow(clippy::indexing_slicing)]
fn squash_attachment_parts(&mut self) {
if self.parts.len() == 2
&& self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
&& self
.parts
.get(1)
.is_some_and(|filepart| match filepart.typ {
if let [textpart, filepart] = &self.parts[..] {
let need_drop = textpart.typ == Viewtype::Text
&& match filepart.typ {
Viewtype::Image
| Viewtype::Gif
| Viewtype::Sticker
@@ -690,24 +680,24 @@ impl MimeMessage {
| Viewtype::File
| Viewtype::Webxdc => true,
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
})
{
let mut parts = std::mem::take(&mut self.parts);
let Some(mut filepart) = parts.pop() else {
// Should never happen.
return;
};
let Some(textpart) = parts.pop() else {
// Should never happen.
return;
};
};
filepart.msg.clone_from(&textpart.msg);
if let Some(quote) = textpart.param.get(Param::Quote) {
filepart.param.set(Param::Quote, quote);
if need_drop {
let mut filepart = self.parts.swap_remove(1);
// insert new one
filepart.msg.clone_from(&self.parts[0].msg);
if let Some(quote) = self.parts[0].param.get(Param::Quote) {
filepart.param.set(Param::Quote, quote);
}
// forget the one we use now
self.parts[0].msg = "".to_string();
// swap new with old
self.parts.push(filepart); // push to the end
let _ = self.parts.swap_remove(0); // drops first element, replacing it with the last one in O(1)
}
self.parts = vec![filepart];
}
}
@@ -1673,8 +1663,17 @@ impl MimeMessage {
.get_header_value(HeaderDef::MessageId)
.and_then(|v| parse_message_id(&v).ok())
{
let mut to_list =
get_all_addresses_from_header(&report.headers, "x-failed-recipients");
let to = if to_list.len() == 1 {
Some(to_list.pop().unwrap())
} else {
None // We do not know which recipient failed
};
return Ok(Some(DeliveryReport {
rfc724_mid: original_message_id,
failed_recipient: to.map(|s| s.addr),
failure,
}));
}
@@ -1752,6 +1751,7 @@ impl MimeMessage {
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
/// Also you should add a test in receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) {
let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
@@ -1772,6 +1772,7 @@ impl MimeMessage {
{
self.delivery_report = Some(DeliveryReport {
rfc724_mid: original_message_id,
failed_recipient: None,
failure: true,
})
}
@@ -1909,20 +1910,22 @@ pub(crate) struct Report {
#[derive(Debug)]
pub(crate) struct DeliveryReport {
pub rfc724_mid: String,
pub failed_recipient: Option<String>,
pub failure: bool,
}
#[allow(clippy::indexing_slicing)]
pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
let mut msgids = Vec::new();
for id in ids.split_whitespace() {
let mut id = id.to_string();
if let Some(id_without_prefix) = id.strip_prefix('<') {
id = id_without_prefix.to_string();
};
if let Some(id_without_suffix) = id.strip_suffix('>') {
id = id_without_suffix.to_string();
};
if id.starts_with('<') {
id = id[1..].to_string();
}
if id.ends_with('>') {
id = id[..id.len() - 1].to_string();
}
if !id.is_empty() {
msgids.push(id);
}
@@ -2274,12 +2277,20 @@ async fn handle_ndn(
let msgs: Vec<_> = context
.sql
.query_map(
"SELECT id FROM msgs
WHERE rfc724_mid=? AND from_id=1",
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" c.type AS type",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
),
(&failed.rfc724_mid,),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
let msg_id: MsgId = row.get("msg_id")?;
let chat_id: ChatId = row.get("chat_id")?;
let chat_type: Chattype = row.get("type")?;
Ok((msg_id, chat_id, chat_type))
},
|rows| Ok(rows.collect::<Vec<_>>()),
)
@@ -2287,13 +2298,16 @@ async fn handle_ndn(
let error = if let Some(error) = error {
error
} else if let Some(failed_recipient) = &failed.failed_recipient {
format!("Delivery to {failed_recipient} failed.").clone()
} else {
"Delivery to at least one recipient failed.".to_string()
};
let err_msg = &error;
let mut first = true;
for msg in msgs {
let msg_id = msg?;
let (msg_id, chat_id, chat_type) = msg?;
let mut message = Message::load_from_db(context, msg_id).await?;
let aggregated_error = message
.error
@@ -2305,13 +2319,51 @@ async fn handle_ndn(
aggregated_error.as_ref().unwrap_or(err_msg),
)
.await?;
if first {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
}
first = false;
}
Ok(())
}
async fn ndn_maybe_add_info_msg(
context: &Context,
failed: &DeliveryReport,
chat_id: ChatId,
chat_type: Chattype,
) -> Result<()> {
match chat_type {
Chattype::Group | Chattype::Broadcast => {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
.await?
.context("contact ID not found")?;
let contact = Contact::get_by_id(context, contact_id).await?;
// Tell the user which of the recipients failed if we know that (because in
// a group, this might otherwise be unclear)
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
context.emit_event(EventType::ChatModified(chat_id));
}
}
Chattype::Mailinglist => {
// ndn_maybe_add_info_msg() is about the case when delivery to the group failed.
// If we get an NDN for the mailing list, just issue a warning.
warn!(context, "ignoring NDN for mailing list.");
}
Chattype::Single => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use mailparse::ParsedMail;
use super::*;
@@ -3100,7 +3152,11 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
// Make sure the file is there even though the html is wrong:
let param = &message.parts[0].param;
let blob: BlobObject = param.get_blob(Param::File, &t).await.unwrap().unwrap();
let blob: BlobObject = param
.get_blob(Param::File, &t, false)
.await
.unwrap()
.unwrap();
let f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap();
let size = f.metadata().await.unwrap().len();
assert_eq!(size, 154);
@@ -3598,10 +3654,9 @@ On 2020-10-25, Bob wrote:
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mime_modified_large_plain() -> Result<()> {
let t = TestContext::new_alice().await;
let t1 = TestContext::new_alice().await;
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
static REPEAT_CNT: usize = DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + 2;
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
@@ -3622,21 +3677,22 @@ On 2020-10-25, Bob wrote:
if draft {
chat.id.set_draft(&t, Some(&mut msg)).await?;
}
let sent_msg = t.send_msg(chat.id, &mut msg).await;
t.send_msg(chat.id, &mut msg).await;
let msg = t.get_last_msg_in(chat.id).await;
assert!(msg.has_html());
let html = msg.id.get_html(&t).await?.unwrap();
assert_eq!(html.matches("<!DOCTYPE html>").count(), 1);
assert_eq!(html.matches("just repeated.<br/>").count(), REPEAT_CNT);
assert_eq!(
msg.id
.get_html(&t)
.await?
.unwrap()
.matches("just repeated")
.count(),
REPEAT_CNT
);
assert!(
msg.text.matches("just repeated.").count()
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
msg.text.matches("just repeated").count() <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
assert!(msg.text.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
let msg = t1.recv_msg(&sent_msg).await;
assert!(msg.has_html());
assert_eq!(msg.id.get_html(&t1).await?.unwrap(), html);
}
t.set_config(Config::Bot, Some("1")).await?;

View File

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

View File

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

View File

@@ -230,7 +230,7 @@ where
.get(9..12)
.context("HTTP status line does not contain a status code")?;
// Interpret status code according to
// Interpert status code according to
// <https://datatracker.ietf.org/doc/html/rfc7231#section-6>.
if status_code == b"407" {
Err(format_err!("Proxy Authentication Required"))

View File

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

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