mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 07:32:12 +03:00
Compare commits
71 Commits
link2xt/pg
...
add-webxdc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e8b5fe3d2 | ||
|
|
70fe196220 | ||
|
|
3ad9cf3c74 | ||
|
|
8ffe864812 | ||
|
|
df8c4cc3e9 | ||
|
|
5a353a206b | ||
|
|
8ddd28d08c | ||
|
|
e07e9aec17 | ||
|
|
0c35360b9f | ||
|
|
c356dbff06 | ||
|
|
d4a6484b0c | ||
|
|
5aa8ffaf5e | ||
|
|
85de1ad538 | ||
|
|
913203fbad | ||
|
|
a42cd5450b | ||
|
|
92a68ceb48 | ||
|
|
ada5368b9c | ||
|
|
f3332fa7a6 | ||
|
|
f03d56143c | ||
|
|
d21756812b | ||
|
|
cbe5c38705 | ||
|
|
755b245495 | ||
|
|
dc5fcdf425 | ||
|
|
45e55c963e | ||
|
|
8967d7748c | ||
|
|
948cefa3ef | ||
|
|
9ec1401a37 | ||
|
|
170b7e2ded | ||
|
|
d63a2b39aa | ||
|
|
167948e62a | ||
|
|
4edade225c | ||
|
|
da546d3526 | ||
|
|
6be96d3eba | ||
|
|
d1537095e4 | ||
|
|
ba68b87c58 | ||
|
|
b5f899540c | ||
|
|
c6dd03590c | ||
|
|
ff3efafcfc | ||
|
|
717c18ed0f | ||
|
|
4026c827be | ||
|
|
cd8cff7efb | ||
|
|
a319c1ea27 | ||
|
|
5db574b44f | ||
|
|
8af90a1299 | ||
|
|
a6db7ba1e3 | ||
|
|
703cad970d | ||
|
|
47757c3c7f | ||
|
|
dca922b932 | ||
|
|
bacdf8f8df | ||
|
|
eed2320217 | ||
|
|
d22c29ab89 | ||
|
|
22b9308c9b | ||
|
|
1f0a12a729 | ||
|
|
d06fa73e4f | ||
|
|
407bc95ae5 | ||
|
|
daeeca3710 | ||
|
|
29de7c3603 | ||
|
|
f669f43fe6 | ||
|
|
8a0c913bbd | ||
|
|
75e1517dcc | ||
|
|
4aad8fb3de | ||
|
|
9640f92327 | ||
|
|
95ac7647ac | ||
|
|
e121fc1389 | ||
|
|
5399cbfffe | ||
|
|
8da1fae51f | ||
|
|
eabf1d15b7 | ||
|
|
3b9e6d6ffa | ||
|
|
8f3be764d2 | ||
|
|
c181db631f | ||
|
|
c18a476806 |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.82.0
|
||||
RUSTUP_TOOLCHAIN: 1.83.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -95,11 +95,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.82.0
|
||||
rust: 1.83.0
|
||||
- os: windows-latest
|
||||
rust: 1.82.0
|
||||
rust: 1.83.0
|
||||
- os: macos-latest
|
||||
rust: 1.82.0
|
||||
rust: 1.83.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
|
||||
82
.github/workflows/nix.yml
vendored
82
.github/workflows/nix.yml
vendored
@@ -2,7 +2,13 @@ name: Test Nix flake
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
push:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -20,3 +26,79 @@ jobs:
|
||||
|
||||
# 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 }}
|
||||
|
||||
140
CHANGELOG.md
140
CHANGELOG.md
@@ -1,5 +1,141 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
@@ -5229,3 +5365,7 @@ 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
|
||||
|
||||
266
CONTRIBUTING.md
266
CONTRIBUTING.md
@@ -1,6 +1,6 @@
|
||||
# Contributing guidelines
|
||||
# Contributing to Delta Chat
|
||||
|
||||
## Reporting bugs
|
||||
## Bug reports
|
||||
|
||||
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,178 +9,114 @@ If the bug you found is specific to
|
||||
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
|
||||
report it to the corresponding repository.
|
||||
|
||||
## Proposing features
|
||||
## Feature proposals
|
||||
|
||||
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
|
||||
|
||||
## Contributing code
|
||||
## Code contributions
|
||||
|
||||
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||
If you want to contribute a code, follow this guide.
|
||||
|
||||
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.
|
||||
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 commited 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).
|
||||
|
||||
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).
|
||||
|
||||
161
Cargo.lock
generated
161
Cargo.lock
generated
@@ -169,9 +169,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.92"
|
||||
version = "1.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13"
|
||||
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
@@ -230,7 +230,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -242,7 +242,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -347,7 +347,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -375,7 +375,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -386,7 +386,7 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"crc32fast",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-lite 2.5.0",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@@ -688,9 +688,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
|
||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1238,7 +1238,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1262,7 +1262,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1273,7 +1273,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1306,7 +1306,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1330,7 +1330,7 @@ dependencies = [
|
||||
"fd-lock",
|
||||
"format-flowed",
|
||||
"futures",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-lite 2.5.0",
|
||||
"hex",
|
||||
"hickory-resolver",
|
||||
"http-body-util",
|
||||
@@ -1372,6 +1372,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sha-1",
|
||||
"sha2",
|
||||
"shadowsocks",
|
||||
"smallvec",
|
||||
"strum",
|
||||
@@ -1406,7 +1407,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -1431,7 +1432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1447,12 +1448,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
"deltachat-jsonrpc",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-lite 2.5.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1471,12 +1472,12 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1526,7 +1527,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1557,7 +1558,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1567,7 +1568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1587,7 +1588,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@@ -1653,7 +1654,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1935,7 +1936,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1955,7 +1956,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2323,9 +2324,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210"
|
||||
checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
|
||||
dependencies = [
|
||||
"fastrand 2.1.1",
|
||||
"futures-core",
|
||||
@@ -2342,7 +2343,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2910,9 +2911,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.4"
|
||||
version = "0.25.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae"
|
||||
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
@@ -3042,7 +3043,7 @@ dependencies = [
|
||||
"derive_more",
|
||||
"ed25519-dalek",
|
||||
"futures-concurrency",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-lite 2.5.0",
|
||||
"futures-util",
|
||||
"indexmap",
|
||||
"iroh-base",
|
||||
@@ -3096,7 +3097,7 @@ dependencies = [
|
||||
"duct",
|
||||
"futures-buffered",
|
||||
"futures-concurrency",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-lite 2.5.0",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"genawaiter",
|
||||
@@ -3217,7 +3218,7 @@ checksum = "c1fd18ec6325dd3f01625f12c01acff50a4374ee1ab708e7b2078885fd63ad30"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-buffered",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-lite 2.5.0",
|
||||
"futures-util",
|
||||
"iroh-net",
|
||||
"tokio",
|
||||
@@ -3302,9 +3303,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kamadak-exif"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99e7b00ff45df279c3e40f7fee99fad4f7eddbf9ed2d24e99133e8683330f0c7"
|
||||
checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837"
|
||||
dependencies = [
|
||||
"mutate_once",
|
||||
]
|
||||
@@ -3692,7 +3693,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-lite 2.5.0",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"libc",
|
||||
@@ -3833,7 +3834,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3915,7 +3916,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3989,7 +3990,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4193,7 +4194,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4291,7 +4292,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4405,7 +4406,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4481,7 +4482,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-lite 2.5.0",
|
||||
"futures-util",
|
||||
"igd-next",
|
||||
"iroh-metrics",
|
||||
@@ -4653,7 +4654,7 @@ dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4691,7 +4692,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4761,9 +4762,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.0"
|
||||
version = "0.37.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbfb3ddf5364c9cfcd65549a1e7b801d0e8d1b14c1a1590a6408aa93cfbfa84"
|
||||
checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -5256,9 +5257,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
version = "0.38.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
@@ -5269,9 +5270,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.14"
|
||||
version = "0.23.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
|
||||
checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -5441,7 +5442,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5564,7 +5565,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5575,7 +5576,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5942,7 +5943,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"struct_iterable_internal",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5970,7 +5971,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6032,9 +6033,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.86"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6072,7 +6073,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6118,9 +6119,9 @@ checksum = "094c9f64d6de9a8506b1e49b63a29333b37ed9e821ee04be694d431b3264c3c5"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.13.0"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
|
||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand 2.1.1",
|
||||
@@ -6156,22 +6157,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.66"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.66"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6287,7 +6288,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6483,7 +6484,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6595,7 +6596,7 @@ dependencies = [
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6817,7 +6818,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -6851,7 +6852,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -6886,9 +6887,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.6"
|
||||
version = "0.26.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
|
||||
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -7004,7 +7005,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7015,7 +7016,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7332,7 +7333,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7358,7 +7359,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7378,7 +7379,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -62,10 +62,10 @@ http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.10"
|
||||
image = { version = "0.25.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.28.1", default-features = false }
|
||||
kamadak-exif = "0.6.0"
|
||||
kamadak-exif = "0.6.1"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = { workspace = true }
|
||||
mailparse = "0.15"
|
||||
@@ -86,12 +86,13 @@ regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.10.0"
|
||||
rustls = { version = "0.23.14", default-features = false }
|
||||
rustls = { version = "0.23.19", 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"
|
||||
@@ -108,7 +109,7 @@ tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.6"
|
||||
webpki-roots = "0.26.7"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -172,7 +173,7 @@ deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.4.0"
|
||||
futures-lite = "2.5.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
nu-ansi-term = "0.46"
|
||||
@@ -184,7 +185,7 @@ rusqlite = "0.32"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.13.0"
|
||||
tempfile = "3.14.0"
|
||||
thiserror = "1"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.11"
|
||||
|
||||
@@ -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 -a v1.116.0`.
|
||||
6. Tag the release: `git tag --annotate v1.116.0`.
|
||||
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||
|
||||
98
STYLE.md
Normal file
98
STYLE.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 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:#}.");
|
||||
```
|
||||
@@ -16,6 +16,7 @@
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1154,9 +1154,14 @@ 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, the actual payload
|
||||
* @param descr The user-visible description of JSON data,
|
||||
* in case of a chess game, e.g. the move.
|
||||
* @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
|
||||
* @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);
|
||||
@@ -2592,13 +2597,15 @@ 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,
|
||||
@@ -4197,6 +4204,11 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* true if the Webxdc should get full internet access, including Webrtc.
|
||||
* currently, this is only true for encrypted Webxdc's in the self chat
|
||||
* that have requested internet access in the manifest.
|
||||
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
|
||||
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
|
||||
+ Should be exposed to `webxdc.sendUpdateMaxSize` in JS land.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
@@ -4482,6 +4494,7 @@ 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()
|
||||
@@ -4511,6 +4524,24 @@ 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.
|
||||
*
|
||||
* 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().
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
char* dc_msg_get_webxdc_href (const dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* Check if a message is still in creation. A message is in creation between
|
||||
* the calls to dc_prepare_msg() and dc_send_msg().
|
||||
@@ -5873,15 +5904,26 @@ 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 by dc_event_get_id().
|
||||
* See also dc_event_get_data1_int() and dc_event_get_data2_int().
|
||||
* 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" 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().
|
||||
* @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().
|
||||
*/
|
||||
char* dc_event_get_data2_str(dc_event_t* event);
|
||||
|
||||
@@ -6079,12 +6121,35 @@ 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
|
||||
*/
|
||||
@@ -6368,6 +6433,25 @@ 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.
|
||||
|
||||
@@ -542,6 +542,7 @@ 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,
|
||||
@@ -568,6 +569,8 @@ 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)]
|
||||
@@ -600,9 +603,12 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
| 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::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -674,6 +680,8 @@ 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 { .. }
|
||||
@@ -681,6 +689,7 @@ 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, .. }
|
||||
@@ -700,6 +709,27 @@ 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() {
|
||||
@@ -748,6 +778,8 @@ 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, .. } => {
|
||||
@@ -775,6 +807,9 @@ 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"),
|
||||
@@ -1059,7 +1094,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()");
|
||||
@@ -1067,14 +1102,10 @@ 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),
|
||||
&to_string_lossy(descr),
|
||||
))
|
||||
.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)))
|
||||
.context("Failed to send webxdc update")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3681,6 +3712,17 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int
|
||||
ffi_msg.message.get_info_type() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_webxdc_href()");
|
||||
return "".strdup();
|
||||
}
|
||||
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_webxdc_href().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -1767,10 +1767,10 @@ impl CommandApi {
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
update_str: String,
|
||||
description: String,
|
||||
_descr: Option<String>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
|
||||
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1829,6 +1829,18 @@ 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
|
||||
|
||||
@@ -106,6 +106,15 @@ pub enum EventType {
|
||||
reaction: String,
|
||||
},
|
||||
|
||||
/// Incoming webxdc info or summary update, should be notified.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingWebxdcNotify {
|
||||
contact_id: u32,
|
||||
msg_id: u32,
|
||||
text: String,
|
||||
href: Option<String>,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -277,6 +286,20 @@ 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 },
|
||||
}
|
||||
@@ -319,6 +342,17 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
reaction: reaction.as_str().to_string(),
|
||||
},
|
||||
CoreEventType::IncomingWebxdcNotify {
|
||||
contact_id,
|
||||
msg_id,
|
||||
text,
|
||||
href,
|
||||
} => IncomingWebxdcNotify {
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
text,
|
||||
href,
|
||||
},
|
||||
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
@@ -409,6 +443,8 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
CoreEventType::AccountsChanged => AccountsChanged,
|
||||
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
|
||||
@@ -85,6 +85,8 @@ pub struct MessageObject {
|
||||
|
||||
webxdc_info: Option<WebxdcMessageInfo>,
|
||||
|
||||
webxdc_href: Option<String>,
|
||||
|
||||
download_state: DownloadState,
|
||||
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
@@ -128,6 +130,11 @@ impl MessageObject {
|
||||
|
||||
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
|
||||
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
|
||||
} else if let (deltachat::mimeparser::SystemMessage::WebxdcInfoMessage, Some(parent_msg)) =
|
||||
(message.get_info_type(), message.parent(context).await?)
|
||||
{
|
||||
// get webcdx info from parent message
|
||||
Some(WebxdcMessageInfo::get_for_message(context, parent_msg.get_id()).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -241,6 +248,10 @@ 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,
|
||||
|
||||
@@ -35,6 +35,14 @@ 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 {
|
||||
@@ -49,7 +57,11 @@ 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 {
|
||||
@@ -59,6 +71,9 @@ impl WebxdcMessageInfo {
|
||||
summary: maybe_empty_string_to_option(summary),
|
||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||
internet_access,
|
||||
self_addr,
|
||||
send_update_interval,
|
||||
send_update_max_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![recursion_limit = "256"]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
pub mod api;
|
||||
pub use yerpc;
|
||||
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.149.0"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -90,6 +90,11 @@ 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)]
|
||||
@@ -102,6 +107,7 @@ mod tests {
|
||||
|
||||
let mut ratelimit = Ratelimit::new_at(Duration::new(60, 0), 3.0, now);
|
||||
assert!(ratelimit.can_send_at(now));
|
||||
assert_eq!(ratelimit.update_interval(), 20_000);
|
||||
|
||||
// Send burst of 3 messages.
|
||||
ratelimit.send_at(now);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
@@ -969,9 +969,7 @@ 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, "this is a webxdc status update")
|
||||
.await?;
|
||||
context.send_webxdc_status_update(msg_id, arg2).await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
@@ -25,7 +25,8 @@ $ pip install .
|
||||
## Testing
|
||||
|
||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
2. Install tox `pip install -U tox`
|
||||
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -24,9 +24,6 @@ classifiers = [
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"imap-tools",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
deltachat_rpc_client = [
|
||||
|
||||
@@ -61,6 +61,8 @@ class EventType(str, Enum):
|
||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
"""
|
||||
Internal Python-level IMAP handling used by the tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import imaplib
|
||||
@@ -11,17 +7,11 @@ import ssl
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
import pytest
|
||||
from imap_tools import AND, Header, MailBox, MailMessage, MailMessageFlags, errors
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Account
|
||||
from deltachat_rpc_client import Account
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
@@ -29,6 +19,8 @@ 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)
|
||||
@@ -212,3 +204,8 @@ 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
|
||||
29
deltachat-rpc-client/tests/test_account_events.py
Normal file
29
deltachat-rpc-client/tests/test_account_events.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
|
||||
def test_event_on_configuration(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
||||
"""
|
||||
|
||||
account = acfactory.new_preconfigured_account()
|
||||
account.clear_all_events()
|
||||
assert not account.is_configured()
|
||||
future = account.configure.future()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
|
||||
break
|
||||
assert account.is_configured()
|
||||
|
||||
future()
|
||||
|
||||
|
||||
# other tests are written in rust: src/tests/account_events.rs
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
|
||||
|
||||
@@ -536,7 +535,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):
|
||||
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
"""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
|
||||
@@ -560,7 +559,7 @@ def test_reactions_for_a_reordering_move(acfactory):
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2_direct_imap = DirectImap(ac2)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
@@ -24,6 +24,9 @@ def test_webxdc(acfactory) -> None:
|
||||
"name": "Chess Board",
|
||||
"sourceCodeUrl": None,
|
||||
"summary": None,
|
||||
"selfAddr": webxdc_info["selfAddr"],
|
||||
"sendUpdateInterval": 1000,
|
||||
"sendUpdateMaxSize": 18874368,
|
||||
}
|
||||
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
|
||||
@@ -16,6 +16,7 @@ deps =
|
||||
pytest
|
||||
pytest-timeout
|
||||
pytest-xdist
|
||||
imap-tools
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.149.0"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -363,6 +363,8 @@
|
||||
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" //
|
||||
@@ -479,8 +481,8 @@
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
|
||||
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat_rpc_server-${manifest.version}.tar.gz'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-${manifest.version}.tar.gz $out'';
|
||||
};
|
||||
|
||||
deltachat-rpc-client =
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
//!
|
||||
//! 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.
|
||||
///
|
||||
|
||||
2453
fuzz/Cargo.lock
generated
2453
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,8 @@ 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,
|
||||
@@ -52,6 +54,7 @@ module.exports = {
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
|
||||
DC_EVENT_INCOMING_REACTION: 2002,
|
||||
DC_EVENT_INCOMING_WEBXDC_NOTIFY: 2003,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
|
||||
@@ -17,6 +17,7 @@ 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',
|
||||
@@ -43,5 +44,7 @@ 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'
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ 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,
|
||||
@@ -52,6 +54,7 @@ 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,
|
||||
@@ -325,6 +328,7 @@ 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',
|
||||
@@ -351,5 +355,7 @@ 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',
|
||||
}
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.149.0"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.149.0"
|
||||
version = "1.151.2"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
|
||||
@@ -705,7 +705,7 @@ class TestOfflineChat:
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
assert qr.startswith("OPENPGP4FPR:")
|
||||
assert qr.startswith("https://i.delta.chat")
|
||||
res = ac2.check_qr(qr)
|
||||
assert res.is_ask_verifycontact()
|
||||
assert not res.is_ask_verifygroup()
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-11-05
|
||||
2024-11-26
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.82.0
|
||||
RUST_VERSION=1.83.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -25,7 +25,7 @@ def build_source_package(version, filename):
|
||||
|
||||
def pack(name, contents):
|
||||
contents = contents.encode()
|
||||
tar_info = tarfile.TarInfo(f"deltachat-rpc-server-{version}/{name}")
|
||||
tar_info = tarfile.TarInfo(f"deltachat_rpc_server-{version}/{name}")
|
||||
tar_info.mode = 0o644
|
||||
tar_info.size = len(contents)
|
||||
pkg.addfile(tar_info, BytesIO(contents))
|
||||
@@ -167,7 +167,7 @@ def main():
|
||||
cargo_manifest = tomllib.load(fp)
|
||||
version = cargo_manifest["package"]["version"]
|
||||
if sys.argv[1] == "source":
|
||||
filename = f"deltachat-rpc-server-{version}.tar.gz"
|
||||
filename = f"deltachat_rpc_server-{version}.tar.gz"
|
||||
build_source_package(version, filename)
|
||||
else:
|
||||
arch = sys.argv[1]
|
||||
|
||||
@@ -139,6 +139,7 @@ impl Accounts {
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
@@ -156,6 +157,7 @@ impl Accounts {
|
||||
.build()
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
@@ -190,6 +192,7 @@ impl Accounts {
|
||||
.context("failed to remove account data")?;
|
||||
}
|
||||
self.config.remove_account(id).await?;
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
130
src/chat.rs
130
src/chat.rs
@@ -567,6 +567,14 @@ 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,
|
||||
@@ -765,27 +773,19 @@ 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
|
||||
.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,))
|
||||
.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(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
@@ -2210,7 +2210,9 @@ 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).await?;
|
||||
context
|
||||
.update_webxdc_integration_database(msg, context)
|
||||
.await?;
|
||||
}
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
Ok(msg.id)
|
||||
@@ -2599,10 +2601,12 @@ impl ChatIdBlocked {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
let protected = peerstate.map_or(false, |p| {
|
||||
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
|
||||
});
|
||||
let 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 smeared_time = create_smeared_timestamp(context);
|
||||
|
||||
let chat_id = context
|
||||
@@ -2985,8 +2989,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
recipients.push(from);
|
||||
}
|
||||
|
||||
// Webxdc integrations are messages, however, shipped with main app and must not be sent out
|
||||
if msg.param.get_int(Param::WebxdcIntegration).is_some() {
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out
|
||||
if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden {
|
||||
recipients.clear();
|
||||
}
|
||||
|
||||
@@ -4653,9 +4657,9 @@ impl Context {
|
||||
Contact::create_ex(self, Nosync, to, addr).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let contact_id = Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None)
|
||||
.await?
|
||||
.with_context(|| format!("No contact for addr '{addr}'"))?;
|
||||
let addr = ContactAddress::new(addr).context("Invalid address")?;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(self, "", &addr, Origin::Hidden).await?;
|
||||
match action {
|
||||
SyncAction::Block => {
|
||||
return contact::set_blocked(self, Nosync, contact_id, true).await
|
||||
@@ -4665,9 +4669,10 @@ impl Context {
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
ChatIdBlocked::lookup_by_contact(self, contact_id)
|
||||
// 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)
|
||||
.await?
|
||||
.with_context(|| format!("No chat for addr '{addr}'"))?
|
||||
.id
|
||||
}
|
||||
SyncId::Grpid(grpid) => {
|
||||
@@ -7473,6 +7478,71 @@ 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;
|
||||
|
||||
@@ -791,6 +791,12 @@ impl Context {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
key,
|
||||
Config::Displayname | Config::Selfavatar | Config::PrivateTag
|
||||
) {
|
||||
self.emit_event(EventType::AccountsItemChanged);
|
||||
}
|
||||
if key.is_synced() {
|
||||
self.emit_event(EventType::ConfigSynced { key });
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ use crate::message::Message;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::{chat, e2ee, provider};
|
||||
use crate::{stock_str, EventType};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
|
||||
macro_rules! progress {
|
||||
@@ -486,6 +486,7 @@ 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)
|
||||
}
|
||||
|
||||
@@ -67,19 +67,15 @@ fn parse_server<B: BufRead>(
|
||||
|
||||
let typ = server_event
|
||||
.attributes()
|
||||
.find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "type"
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.find_map(|attr| {
|
||||
attr.ok().filter(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("type")
|
||||
})
|
||||
})
|
||||
.map(|typ| {
|
||||
typ.unwrap()
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
typ.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
})
|
||||
|
||||
@@ -3244,4 +3244,20 @@ Until the false-positive is fixed:
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use async_channel::{self as channel, Receiver, Sender};
|
||||
use pgp::types::PublicKeyTrait;
|
||||
use pgp::SignedPublicKey;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, Notify, OnceCell, RwLock};
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
|
||||
@@ -276,7 +276,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: std::sync::RwLock<String>,
|
||||
pub(crate) last_error: parking_lot::RwLock<String>,
|
||||
|
||||
/// If debug logging is enabled, this contains all necessary information
|
||||
///
|
||||
@@ -292,7 +292,7 @@ pub struct InnerContext {
|
||||
pub(crate) push_subscribed: AtomicBool,
|
||||
|
||||
/// Iroh for realtime peer channels.
|
||||
pub(crate) iroh: OnceCell<Iroh>,
|
||||
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -446,11 +446,11 @@ impl Context {
|
||||
metadata: RwLock::new(None),
|
||||
creation_time: tools::Time::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
last_error: std::sync::RwLock::new("".to_string()),
|
||||
last_error: parking_lot::RwLock::new("".to_string()),
|
||||
debug_logging: std::sync::RwLock::new(None),
|
||||
push_subscriber,
|
||||
push_subscribed: AtomicBool::new(false),
|
||||
iroh: OnceCell::new(),
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -472,12 +472,33 @@ 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
|
||||
@@ -488,7 +509,7 @@ impl Context {
|
||||
|
||||
/// Indicate that the network likely has come back.
|
||||
pub async fn maybe_network(&self) {
|
||||
if let Some(iroh) = self.iroh.get() {
|
||||
if let Some(ref iroh) = *self.iroh.read().await {
|
||||
iroh.network_change().await;
|
||||
}
|
||||
self.scheduler.maybe_network().await;
|
||||
@@ -2064,4 +2085,41 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Change the config circumventing the cache
|
||||
// This simulates what the notification plugin on iOS might do
|
||||
// because it runs in a different process
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice's Delta Chat doesn't know about it yet:
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Starting IO will fail of course because no server settings are configured,
|
||||
// but it should invalidate the caches:
|
||||
alice.start_io().await;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,11 @@ 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,
|
||||
)
|
||||
|
||||
@@ -440,7 +440,7 @@ mod tests {
|
||||
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
|
||||
|
||||
alice
|
||||
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#, "d")
|
||||
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#)
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -107,6 +107,21 @@ pub enum EventType {
|
||||
reaction: Reaction,
|
||||
},
|
||||
|
||||
/// A webxdc wants an info message or a changed summary to be notified.
|
||||
IncomingWebxdcNotify {
|
||||
/// ID of the contact sending.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// ID of the added info message or webxdc instance in case of summary change.
|
||||
msg_id: MsgId,
|
||||
|
||||
/// Text to notify.
|
||||
text: String,
|
||||
|
||||
/// Link assigned to this notification, if any.
|
||||
href: Option<String>,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -332,6 +347,20 @@ 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,
|
||||
|
||||
@@ -426,6 +426,7 @@ 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)
|
||||
|
||||
10
src/key.rs
10
src/key.rs
@@ -382,9 +382,9 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
/// Creates new 160-bit (20 bytes) or 256-bit (32 bytes) fingerprint.
|
||||
/// Creates new 160-bit (20 bytes) fingerprint.
|
||||
pub fn new(v: Vec<u8>) -> Fingerprint {
|
||||
debug_assert!(v.len() == 20 || v.len() == 32);
|
||||
debug_assert_eq!(v.len(), 20);
|
||||
Fingerprint(v)
|
||||
}
|
||||
|
||||
@@ -438,11 +438,7 @@ impl std::str::FromStr for Fingerprint {
|
||||
.filter(|&c| c.is_ascii_hexdigit())
|
||||
.collect();
|
||||
let v: Vec<u8> = hex::decode(&hex_repr)?;
|
||||
ensure!(
|
||||
v.len() == 20 || v.len() == 32,
|
||||
"wrong fingerprint length: {}",
|
||||
hex_repr
|
||||
);
|
||||
ensure!(v.len() == 20, "wrong fingerprint length: {}", hex_repr);
|
||||
let fp = Fingerprint::new(v);
|
||||
Ok(fp)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
|
||||
@@ -244,18 +244,14 @@ 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(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "accuracy"
|
||||
})
|
||||
.unwrap_or_default()
|
||||
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")
|
||||
})
|
||||
}) {
|
||||
let v = acc
|
||||
.unwrap()
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
@@ -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().unwrap();
|
||||
let mut last_error = self.last_error.write();
|
||||
*last_error = error.to_string();
|
||||
}
|
||||
|
||||
/// Get last error string.
|
||||
pub fn get_last_error(&self) -> String {
|
||||
let last_error = &*self.last_error.read().unwrap();
|
||||
let last_error = &*self.last_error.read();
|
||||
last_error.clone()
|
||||
}
|
||||
}
|
||||
|
||||
117
src/message.rs
117
src/message.rs
@@ -1693,6 +1693,7 @@ 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,
|
||||
@@ -1708,6 +1709,7 @@ 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")?;
|
||||
@@ -1719,6 +1721,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
download_state,
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
@@ -1748,6 +1751,7 @@ 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,
|
||||
@@ -1757,7 +1761,14 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
_curr_ephemeral_timer,
|
||||
) in msgs
|
||||
{
|
||||
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||||
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 {
|
||||
update_msg_state(context, id, MessageState::InSeen).await?;
|
||||
info!(context, "Seen message {}.", id);
|
||||
|
||||
@@ -2583,6 +2594,110 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_not_downloaded_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
|
||||
.await?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
// A not downloaded message can be seen only if it's seen on another device.
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
// Marking the message as seen again is a no op.
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::InProgress)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Failure)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Undecipherable)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
assert!(
|
||||
!alice
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?
|
||||
);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
// Let's assume that Alice and Bob resolved the problem with encryption.
|
||||
let old_msg = msg;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, old_msg.chat_id);
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
// The message state mustn't be downgraded to `InFresh`.
|
||||
assert_eq!(msg.state, MessageState::InNoticed);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
let msg = Message::load_from_db(alice, msg.id).await?;
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
|
||||
.await?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let seen = true;
|
||||
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
|
||||
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_state() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
@@ -6,7 +6,6 @@ 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;
|
||||
|
||||
@@ -1300,9 +1299,18 @@ impl MimeFactory {
|
||||
|
||||
let final_text = placeholdertext.as_deref().unwrap_or(&msg.text);
|
||||
|
||||
let mut quoted_text = msg
|
||||
.quoted_text()
|
||||
.map(|quote| format_flowed_quote("e) + "\r\n\r\n");
|
||||
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)
|
||||
}
|
||||
|
||||
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());
|
||||
@@ -1312,7 +1320,6 @@ 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;
|
||||
|
||||
@@ -1322,7 +1329,7 @@ impl MimeFactory {
|
||||
"{}{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
quoted_text.unwrap_or_default(),
|
||||
escape_message_footer_marks(&flowed_text),
|
||||
escape_message_footer_marks(final_text),
|
||||
if !final_text.is_empty() && !footer.is_empty() {
|
||||
"\r\n\r\n"
|
||||
} else {
|
||||
@@ -1332,10 +1339,8 @@ impl MimeFactory {
|
||||
footer
|
||||
);
|
||||
|
||||
let mut main_part = PartBuilder::new().header((
|
||||
"Content-Type",
|
||||
"text/plain; charset=utf-8; format=flowed; delsp=no",
|
||||
));
|
||||
let mut main_part =
|
||||
PartBuilder::new().header(("Content-Type", "text/plain; charset=utf-8"));
|
||||
main_part = self.add_message_text(main_part, message_text);
|
||||
|
||||
if is_reaction {
|
||||
|
||||
@@ -1667,10 +1667,11 @@ impl MimeMessage {
|
||||
{
|
||||
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())
|
||||
let to = if to_list.len() != 1 {
|
||||
// We do not know which recipient failed
|
||||
None
|
||||
} else {
|
||||
None // We do not know which recipient failed
|
||||
to_list.pop()
|
||||
};
|
||||
|
||||
return Ok(Some(DeliveryReport {
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
//! Adding peer channels to webxdc needs upfront negotation of a topic and sharing of public keys so that
|
||||
//! nodes can connect to each other. The explicit approach is as follows:
|
||||
//!
|
||||
//! 1. We introduce a new [GossipTopic](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
|
||||
//! 1. We introduce a new [`IrohGossipTopic`](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
|
||||
//! securely generated on the initial webxdc sender's device. This message header is encrypted
|
||||
//! and sent in the same message as the webxdc application.
|
||||
//! 2. Whenever `joinRealtimeChannel().setListener()` or `joinRealtimeChannel().send()` is called by the webxdc application,
|
||||
//! we start a routine to establish p2p connectivity and join the gossip swarm with Iroh.
|
||||
//! 3. The first step of this routine is to introduce yourself with a regular message containing the `IrohPublicKey`.
|
||||
//! 3. The first step of this routine is to introduce yourself with a regular message containing the [`IrohNodeAddr`](crate::headerdef::HeaderDef::IrohNodeAddr).
|
||||
//! This message contains the users relay-server and public key.
|
||||
//! Direct IP address is not included as this information can be persisted by email providers.
|
||||
//! 4. After the announcement, the sending peer joins the gossip swarm with an empty list of peer IDs (as they don't know anyone yet).
|
||||
@@ -78,6 +78,14 @@ impl Iroh {
|
||||
self.endpoint.network_change().await
|
||||
}
|
||||
|
||||
/// Closes the QUIC endpoint.
|
||||
pub(crate) async fn close(self) -> Result<()> {
|
||||
self.endpoint
|
||||
.close(0u32.into(), b"")
|
||||
.await
|
||||
.context("Closing iroh endpoint failed")
|
||||
}
|
||||
|
||||
/// Join a topic and create the subscriber loop for it.
|
||||
///
|
||||
/// If there is no gossip, create it.
|
||||
@@ -285,15 +293,36 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Get or initialize the iroh peer channel.
|
||||
pub async fn get_or_try_init_peer_channel(&self) -> Result<&Iroh> {
|
||||
pub async fn get_or_try_init_peer_channel(
|
||||
&self,
|
||||
) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
|
||||
if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
bail!("Attempt to get Iroh when realtime is disabled");
|
||||
}
|
||||
|
||||
let ctx = self.clone();
|
||||
self.iroh
|
||||
.get_or_try_init(|| async { ctx.init_peer_channels().await })
|
||||
.await
|
||||
if let Ok(lock) = tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
|
||||
self.iroh.read().await,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
) {
|
||||
return Ok(lock);
|
||||
}
|
||||
|
||||
let lock = self.iroh.write().await;
|
||||
match tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
|
||||
lock,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
) {
|
||||
Ok(lock) => Ok(lock),
|
||||
Err(mut lock) => {
|
||||
let iroh = self.init_peer_channels().await?;
|
||||
*lock = Some(iroh);
|
||||
tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
|
||||
lock,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
)
|
||||
.map_err(|_| anyhow!("Downgrade should succeed as we just stored `Some` value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,7 +655,6 @@ mod tests {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
|
||||
|
||||
// Bob adds alice to gossip peers.
|
||||
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
|
||||
@@ -636,13 +664,23 @@ mod tests {
|
||||
.map(|addr| addr.node_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let alice_iroh = alice.get_or_try_init_peer_channel().await.unwrap();
|
||||
assert_eq!(
|
||||
members,
|
||||
vec![alice_iroh.get_node_addr().await.unwrap().node_id]
|
||||
vec![
|
||||
alice
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
]
|
||||
);
|
||||
|
||||
bob_iroh
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.join_and_subscribe_gossip(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -651,7 +689,10 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Alice sends ephemeral message
|
||||
alice_iroh
|
||||
alice
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -670,7 +711,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
// Bob sends ephemeral message
|
||||
bob_iroh
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -699,10 +742,20 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
vec![bob_iroh.get_node_addr().await.unwrap().node_id]
|
||||
vec![
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
]
|
||||
);
|
||||
|
||||
bob_iroh
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -720,6 +773,12 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calling stop_io() closes iroh endpoint as well,
|
||||
// even though I/O was not started in this test.
|
||||
assert!(alice.iroh.read().await.is_some());
|
||||
alice.stop_io().await;
|
||||
assert!(alice.iroh.read().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -761,7 +820,6 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
|
||||
|
||||
// Bob adds alice to gossip peers.
|
||||
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
|
||||
@@ -771,13 +829,23 @@ mod tests {
|
||||
.map(|addr| addr.node_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let alice_iroh = alice.get_or_try_init_peer_channel().await.unwrap();
|
||||
assert_eq!(
|
||||
members,
|
||||
vec![alice_iroh.get_node_addr().await.unwrap().node_id]
|
||||
vec![
|
||||
alice
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
]
|
||||
);
|
||||
|
||||
bob_iroh
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.join_and_subscribe_gossip(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -786,7 +854,10 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Alice sends ephemeral message
|
||||
alice_iroh
|
||||
alice
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -811,7 +882,9 @@ mod tests {
|
||||
.unwrap();
|
||||
let bob_sequence_number = bob
|
||||
.iroh
|
||||
.get()
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.sequence_numbers
|
||||
.lock()
|
||||
@@ -820,7 +893,9 @@ mod tests {
|
||||
leave_webxdc_realtime(bob, bob_webxdc.id).await.unwrap();
|
||||
let bob_sequence_number_after = bob
|
||||
.iroh
|
||||
.get()
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.sequence_numbers
|
||||
.lock()
|
||||
@@ -829,7 +904,9 @@ mod tests {
|
||||
// Check that sequence number is persisted when leaving the channel.
|
||||
assert_eq!(bob_sequence_number, bob_sequence_number_after);
|
||||
|
||||
bob_iroh
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.join_and_subscribe_gossip(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -837,7 +914,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
bob_iroh
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -860,7 +939,16 @@ mod tests {
|
||||
// bob for example does not change the channels because he never sends an
|
||||
// advertisement
|
||||
assert_eq!(
|
||||
alice.iroh.get().unwrap().iroh_channels.read().await.len(),
|
||||
alice
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iroh_channels
|
||||
.read()
|
||||
.await
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
leave_webxdc_realtime(alice, alice_webxdc.id).await.unwrap();
|
||||
@@ -870,7 +958,9 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(alice
|
||||
.iroh
|
||||
.get()
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iroh_channels
|
||||
.read()
|
||||
@@ -963,19 +1053,19 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
|
||||
// This internal function should return error
|
||||
// if accidentally called with the setting disabled.
|
||||
|
||||
@@ -542,6 +542,8 @@ impl Peerstate {
|
||||
/// * `old_addr`: Old address of the peerstate in case of an AEAP transition.
|
||||
pub(crate) async fn save_to_db_ex(&self, sql: &Sql, old_addr: Option<&str>) -> Result<()> {
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let verified_key_fingerprint =
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex());
|
||||
if let Some(old_addr) = old_addr {
|
||||
// We are doing an AEAP transition to the new address and the SQL INSERT below will
|
||||
// save the existing peerstate as belonging to this new address. We now need to
|
||||
@@ -551,11 +553,14 @@ impl Peerstate {
|
||||
// existing peerstate as this would break encryption to it. This is critical for
|
||||
// non-verified groups -- if we can't encrypt to the old address, we can't securely
|
||||
// remove it from the group (to add the new one instead).
|
||||
//
|
||||
// NB: We check that `verified_key_fingerprint` hasn't changed to protect from
|
||||
// possible races.
|
||||
t.execute(
|
||||
"UPDATE acpeerstates \
|
||||
SET verified_key=NULL, verified_key_fingerprint='', verifier='' \
|
||||
WHERE addr=?",
|
||||
(old_addr,),
|
||||
"UPDATE acpeerstates
|
||||
SET verified_key=NULL, verified_key_fingerprint='', verifier=''
|
||||
WHERE addr=? AND verified_key_fingerprint=?",
|
||||
(old_addr, &verified_key_fingerprint),
|
||||
)?;
|
||||
}
|
||||
t.execute(
|
||||
@@ -604,7 +609,7 @@ impl Peerstate {
|
||||
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
&verified_key_fingerprint,
|
||||
self.verifier.as_deref().unwrap_or(""),
|
||||
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.secondary_verified_key_fingerprint
|
||||
|
||||
@@ -11,6 +11,7 @@ use pgp::composed::{
|
||||
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
|
||||
SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder,
|
||||
};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey};
|
||||
@@ -186,12 +187,14 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
|
||||
let (signing_key_type, encryption_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::Ed25519, PgpKeyType::X25519),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (
|
||||
PgpKeyType::EdDSALegacy,
|
||||
PgpKeyType::ECDH(ECCCurve::Curve25519),
|
||||
),
|
||||
};
|
||||
|
||||
let user_id = format!("<{addr}>");
|
||||
let key_params = SecretKeyParamsBuilder::default()
|
||||
.version(pgp::types::KeyVersion::V6)
|
||||
.key_type(signing_key_type)
|
||||
.can_certify(true)
|
||||
.can_sign(true)
|
||||
@@ -215,7 +218,6 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
|
||||
])
|
||||
.subkey(
|
||||
SubkeyParamsBuilder::default()
|
||||
.version(pgp::types::KeyVersion::V6)
|
||||
.key_type(encryption_key_type)
|
||||
.can_encrypt(true)
|
||||
.passphrase(None)
|
||||
|
||||
31
src/qr.rs
31
src/qr.rs
@@ -270,6 +270,7 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
|
||||
/// The function should be called after a QR code is scanned.
|
||||
/// The function takes the raw text scanned and checks what can be done with it.
|
||||
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let qr = qr.trim();
|
||||
let qrcode = if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
|
||||
decode_openpgp(context, qr)
|
||||
.await
|
||||
@@ -807,11 +808,7 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
.get(MAILTO_SCHEME.len()..)
|
||||
.context("Invalid mailto: scheme")?;
|
||||
|
||||
let (addr, query) = if let Some(query_index) = payload.find('?') {
|
||||
(&payload[..query_index], &payload[query_index + 1..])
|
||||
} else {
|
||||
(payload, "")
|
||||
};
|
||||
let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
|
||||
|
||||
let param: BTreeMap<&str, &str> = query
|
||||
.split('&')
|
||||
@@ -861,12 +858,9 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
|
||||
|
||||
let addr = if let Some(query_index) = payload.find(':') {
|
||||
&payload[..query_index]
|
||||
} else {
|
||||
bail!("Invalid SMTP found");
|
||||
};
|
||||
|
||||
let (addr, _rest) = payload
|
||||
.split_once(':')
|
||||
.context("Invalid SMTP scheme payload")?;
|
||||
let addr = normalize_address(addr)?;
|
||||
let name = "";
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
@@ -1001,6 +995,17 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
// Test that QR code whitespace is stripped.
|
||||
// Users can copy-paste QR code contents and "scan"
|
||||
// from the clipboard.
|
||||
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1750,7 +1755,9 @@ mod tests {
|
||||
);
|
||||
|
||||
// Test URL without port.
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
|
||||
//
|
||||
// Also check that whitespace is trimmed.
|
||||
let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
|
||||
@@ -558,7 +558,12 @@ Here's my footer -- bob@example.net"
|
||||
) -> Result<()> {
|
||||
let event = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ReactionsChanged { .. }))
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::ReactionsChanged { .. } | EventType::IncomingMsg { .. }
|
||||
)
|
||||
})
|
||||
.await;
|
||||
match event {
|
||||
EventType::ReactionsChanged {
|
||||
@@ -570,7 +575,7 @@ Here's my footer -- bob@example.net"
|
||||
assert_eq!(msg_id, expected_msg_id);
|
||||
assert_eq!(contact_id, expected_contact_id);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
_ => panic!("Unexpected event {event:?}."),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -583,7 +588,14 @@ Here's my footer -- bob@example.net"
|
||||
) -> Result<()> {
|
||||
let event = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingReaction { .. }))
|
||||
// Check for absence of `IncomingMsg` events -- it appeared that it's quite easy to make
|
||||
// bugs when `IncomingMsg` is issued for reactions.
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
|
||||
)
|
||||
})
|
||||
.await;
|
||||
match event {
|
||||
EventType::IncomingReaction {
|
||||
@@ -595,16 +607,25 @@ Here's my footer -- bob@example.net"
|
||||
assert_eq!(contact_id, expected_contact_id);
|
||||
assert_eq!(reaction, Reaction::from(expected_reaction));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
_ => panic!("Unexpected event {event:?}."),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn has_incoming_reactions_event(t: &TestContext) -> bool {
|
||||
t.evtracker
|
||||
.get_matching_opt(t, |evt| matches!(evt, EventType::IncomingReaction { .. }))
|
||||
.await
|
||||
.is_some()
|
||||
/// Checks that no unwanted events remain after expecting "wanted" reaction events.
|
||||
async fn expect_no_unwanted_events(t: &TestContext) {
|
||||
let ev = t
|
||||
.evtracker
|
||||
.get_matching_opt(t, |evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
|
||||
)
|
||||
})
|
||||
.await;
|
||||
if let Some(ev) = ev {
|
||||
panic!("Unwanted event {ev:?}.")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -635,9 +656,10 @@ Here's my footer -- bob@example.net"
|
||||
|
||||
bob_msg.chat_id.accept(&bob).await?;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
|
||||
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
|
||||
assert!(!has_incoming_reactions_event(&bob).await);
|
||||
expect_no_unwanted_events(&bob).await;
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
|
||||
|
||||
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||
@@ -656,6 +678,7 @@ Here's my footer -- bob@example.net"
|
||||
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
|
||||
.await?;
|
||||
expect_incoming_reactions_event(&alice, alice_msg.sender_msg_id, *bob_id, "👍").await?;
|
||||
expect_no_unwanted_events(&alice).await;
|
||||
|
||||
// Alice reacts to own message.
|
||||
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
|
||||
@@ -684,6 +707,7 @@ Here's my footer -- bob@example.net"
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice.set_config(Config::Displayname, Some("ALICE")).await?;
|
||||
bob.set_config(Config::Displayname, Some("BOB")).await?;
|
||||
let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
|
||||
// Alice sends message to Bob
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
@@ -696,7 +720,9 @@ Here's my footer -- bob@example.net"
|
||||
send_reaction(&bob, bob_msg1.id, "👍").await?;
|
||||
let bob_send_reaction = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&bob_send_reaction).await;
|
||||
assert!(has_incoming_reactions_event(&alice).await);
|
||||
expect_incoming_reactions_event(&alice, alice_msg1.sender_msg_id, alice_bob_id, "👍")
|
||||
.await?;
|
||||
expect_no_unwanted_events(&alice).await;
|
||||
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&bob, 0, None).await?;
|
||||
@@ -711,8 +737,9 @@ Here's my footer -- bob@example.net"
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?;
|
||||
let alice_send_reaction = alice.pop_sent_msg().await;
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg_opt(&alice_send_reaction).await;
|
||||
assert!(!has_incoming_reactions_event(&bob).await);
|
||||
expect_no_unwanted_events(&bob).await;
|
||||
|
||||
assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await;
|
||||
assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await;
|
||||
@@ -934,7 +961,9 @@ Here's my footer -- bob@example.net"
|
||||
expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
|
||||
expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
|
||||
.await?;
|
||||
|
||||
for a in [&alice0, &alice1] {
|
||||
expect_no_unwanted_events(a).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::str::FromStr;
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress};
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mailparse::{parse_mail, SingleInfo};
|
||||
use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
@@ -73,12 +73,13 @@ pub struct ReceivedMsg {
|
||||
///
|
||||
/// This method returns errors on a failure to parse the mail or extract Message-ID. It's only used
|
||||
/// for tests and REPL tool, not actual message reception pipeline.
|
||||
#[cfg(any(test, feature = "internals"))]
|
||||
pub async fn receive_imf(
|
||||
context: &Context,
|
||||
imf_raw: &[u8],
|
||||
seen: bool,
|
||||
) -> Result<Option<ReceivedMsg>> {
|
||||
let mail = parse_mail(imf_raw).context("can't parse mail")?;
|
||||
let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
|
||||
let rfc724_mid =
|
||||
imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
|
||||
if let Some(download_limit) = context.download_limit().await? {
|
||||
@@ -105,6 +106,7 @@ pub async fn receive_imf(
|
||||
/// Emulates reception of a message from "INBOX".
|
||||
///
|
||||
/// Only used for tests and REPL tool, not actual message reception pipeline.
|
||||
#[cfg(any(test, feature = "internals"))]
|
||||
pub(crate) async fn receive_imf_from_inbox(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
@@ -1575,7 +1577,7 @@ INSERT INTO msgs
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
|
||||
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
|
||||
type=excluded.type, msgrmsg=excluded.msgrmsg,
|
||||
type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
|
||||
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
|
||||
txt_raw=excluded.txt_raw, param=excluded.param,
|
||||
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
|
||||
|
||||
@@ -1068,7 +1068,7 @@ async fn test_classic_mailing_list() -> Result<()> {
|
||||
let mime = sent.payload();
|
||||
|
||||
println!("Sent mime message is:\n\n{mime}\n\n");
|
||||
assert!(mime.contains("Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no\r\n"));
|
||||
assert!(mime.contains("Content-Type: text/plain; charset=utf-8\r\n"));
|
||||
assert!(mime.contains("Subject: =?utf-8?q?Re=3A_=5Bdelta-dev=5D_What=27s_up=3F?=\r\n"));
|
||||
assert!(mime.contains("MIME-Version: 1.0\r\n"));
|
||||
assert!(mime.contains("In-Reply-To: <38942@posteo.org>\r\n"));
|
||||
|
||||
@@ -104,7 +104,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
||||
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
&group_name_urlencoded,
|
||||
@@ -119,7 +119,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
||||
"https://i.delta.chat/#{}&a={}&n={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
self_name_urlencoded,
|
||||
@@ -244,10 +244,10 @@ async fn verify_sender_by_fingerprint(
|
||||
|
||||
/// What to do with a Secure-Join handshake message after it was handled.
|
||||
///
|
||||
/// This status is returned to [`receive_imf`] which will use it to decide what to do
|
||||
/// This status is returned to [`receive_imf_inner`] which will use it to decide what to do
|
||||
/// next with this incoming setup-contact/secure-join handshake message.
|
||||
///
|
||||
/// [`receive_imf`]: crate::receive_imf::receive_imf
|
||||
/// [`receive_imf_inner`]: crate::receive_imf::receive_imf_inner
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum HandshakeMessage {
|
||||
/// The message has been fully handled and should be removed/delete.
|
||||
@@ -347,7 +347,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
send_alice_handshake_msg(
|
||||
context,
|
||||
contact_id,
|
||||
&format!("{}-auth-required", &step[..2]),
|
||||
&format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
|
||||
)
|
||||
.await
|
||||
.context("failed sending auth-required handshake message")?;
|
||||
|
||||
47
src/smtp.rs
47
src/smtp.rs
@@ -244,32 +244,27 @@ pub(crate) async fn smtp_send(
|
||||
async_smtp::error::Error::Transient(ref response) => {
|
||||
// We got a transient 4xx response from SMTP server.
|
||||
// Give some time until the server-side error maybe goes away.
|
||||
|
||||
if let Some(first_word) = response.first_word() {
|
||||
if first_word.ends_with(".1.1")
|
||||
|| first_word.ends_with(".1.2")
|
||||
|| first_word.ends_with(".1.3")
|
||||
{
|
||||
// Sometimes we receive transient errors that should be permanent.
|
||||
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
|
||||
// receive as a transient error are misconfigurations of the smtp server.
|
||||
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
|
||||
info!(context, "Received extended status code {first_word} for a transient error. This looks like a misconfigured SMTP server, let's fail immediately.");
|
||||
SendResult::Failure(format_err!("Permanent SMTP error: {}", err))
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Transient error with status code {first_word}, postponing retry for later."
|
||||
);
|
||||
SendResult::Retry
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Transient error without status code, postponing retry for later."
|
||||
);
|
||||
SendResult::Retry
|
||||
}
|
||||
//
|
||||
// One particular case is
|
||||
// `450 4.1.2 <alice@example.org>: Recipient address rejected: Domain not found`.
|
||||
// known to be returned by Postfix.
|
||||
//
|
||||
// [RFC 3463](https://tools.ietf.org/html/rfc3463#section-3.2)
|
||||
// says "This code is only useful for permanent failures."
|
||||
// in X.1.1, X.1.2 and X.1.3 descriptions.
|
||||
//
|
||||
// Previous Delta Chat core versions
|
||||
// from 1.51.0 to 1.151.1
|
||||
// were treating such errors as permanent.
|
||||
//
|
||||
// This was later reverted because such errors were observed
|
||||
// for existing domains and turned out to be actually transient,
|
||||
// likely caused by nameserver downtime.
|
||||
info!(
|
||||
context,
|
||||
"Transient error {response:?}, postponing retry for later."
|
||||
);
|
||||
SendResult::Retry
|
||||
}
|
||||
_ => {
|
||||
info!(
|
||||
|
||||
@@ -1070,6 +1070,24 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 124)?;
|
||||
if dbversion < migration_version {
|
||||
// Mark Saved Messages chat as protected if it already exists.
|
||||
sql.execute_migration(
|
||||
"UPDATE chats
|
||||
SET protected=1 -- ProtectionStatus::Protected
|
||||
WHERE type==100 -- Chattype::Single
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM chats_contacts cc
|
||||
WHERE cc.chat_id==chats.id
|
||||
AND cc.contact_id=1
|
||||
)
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -289,7 +289,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
use crate::param::Param;
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
|
||||
assert_eq!(msg.get_summary_text(ctx).await, expected);
|
||||
@@ -298,7 +298,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_summary_text() {
|
||||
let d = test::TestContext::new().await;
|
||||
let d = TestContext::new_alice().await;
|
||||
let ctx = &d.ctx;
|
||||
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
|
||||
.await
|
||||
|
||||
@@ -391,7 +391,7 @@ impl TestContext {
|
||||
Self {
|
||||
ctx,
|
||||
dir,
|
||||
evtracker: EventTracker(evtracker_receiver),
|
||||
evtracker: EventTracker::new(evtracker_receiver),
|
||||
_log_sink,
|
||||
}
|
||||
}
|
||||
@@ -655,8 +655,8 @@ impl TestContext {
|
||||
.expect("failed to load msg")
|
||||
}
|
||||
|
||||
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
|
||||
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
|
||||
/// Returns the [`ContactId`] for the other [`TestContext`], creating a contact if necessary.
|
||||
pub async fn add_or_lookup_contact_id(&self, other: &TestContext) -> ContactId {
|
||||
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
|
||||
let addr = ContactAddress::new(&primary_self_addr).unwrap();
|
||||
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
|
||||
@@ -670,6 +670,12 @@ impl TestContext {
|
||||
Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr),
|
||||
Modifier::Created => warn!(&self.ctx, "Contact {} created by TestContext", &addr),
|
||||
}
|
||||
contact_id
|
||||
}
|
||||
|
||||
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
|
||||
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
|
||||
let contact_id = self.add_or_lookup_contact_id(other).await;
|
||||
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
|
||||
}
|
||||
|
||||
@@ -1087,6 +1093,10 @@ impl DerefMut for EventTracker {
|
||||
}
|
||||
|
||||
impl EventTracker {
|
||||
pub fn new(emitter: EventEmitter) -> Self {
|
||||
Self(emitter)
|
||||
}
|
||||
|
||||
/// Consumes emitted events returning the first matching one.
|
||||
///
|
||||
/// If no matching events are ready this will wait for new events to arrive and time out
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
mod account_events;
|
||||
mod aeap;
|
||||
mod verified_chats;
|
||||
|
||||
170
src/tests/account_events.rs
Normal file
170
src/tests/account_events.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
//! contains tests for account (list) events
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::accounts::Accounts;
|
||||
use crate::config::Config;
|
||||
use crate::imex::{get_backup, has_backup, imex, BackupProvider, ImexMode};
|
||||
use crate::test_utils::{sync, EventTracker, TestContext, TestContextManager};
|
||||
use crate::EventType;
|
||||
|
||||
async fn wait_for_item_changed(context: &TestContext) {
|
||||
context
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::AccountsItemChanged))
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_account_event() -> Result<()> {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut manager = Accounts::new(dir.path().join("accounts"), true).await?;
|
||||
let tracker = EventTracker::new(manager.get_event_emitter());
|
||||
|
||||
// create account
|
||||
tracker.clear_events();
|
||||
let account_id = manager.add_account().await?;
|
||||
tracker
|
||||
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
|
||||
.await;
|
||||
|
||||
// remove account
|
||||
tracker.clear_events();
|
||||
manager.remove_account(account_id).await?;
|
||||
tracker
|
||||
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
|
||||
.await;
|
||||
|
||||
// create closed account
|
||||
tracker.clear_events();
|
||||
manager.add_closed_account().await?;
|
||||
tracker
|
||||
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// configuration is tested by python tests in deltachat-rpc-client/tests/test_account_events.py
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_displayname() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context = tcm.alice().await;
|
||||
context.evtracker.clear_events();
|
||||
context
|
||||
.set_config(crate::config::Config::Displayname, Some("🐰 Alice"))
|
||||
.await?;
|
||||
wait_for_item_changed(&context).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_selfavatar() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context = tcm.alice().await;
|
||||
let file = context.dir.path().join("avatar.jpg");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
context.evtracker.clear_events();
|
||||
context
|
||||
.set_config(
|
||||
crate::config::Config::Selfavatar,
|
||||
Some(file.to_str().unwrap()),
|
||||
)
|
||||
.await?;
|
||||
wait_for_item_changed(&context).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_private_tag() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context = tcm.alice().await;
|
||||
context.evtracker.clear_events();
|
||||
context
|
||||
.set_config(crate::config::Config::PrivateTag, Some("Wonderland"))
|
||||
.await?;
|
||||
wait_for_item_changed(&context).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_import_backup() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context1 = tcm.alice().await;
|
||||
let backup_dir = tempfile::tempdir().unwrap();
|
||||
assert!(
|
||||
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
let context2 = TestContext::new().await;
|
||||
assert!(!context2.is_configured().await?);
|
||||
context2.evtracker.clear_events();
|
||||
let backup = has_backup(&context2, backup_dir.path()).await?;
|
||||
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None).await?;
|
||||
assert!(context2.is_configured().await?);
|
||||
wait_for_item_changed(&context2).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_receive_backup() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
// Create first device.
|
||||
let ctx0 = tcm.alice().await;
|
||||
// Prepare to transfer backup.
|
||||
let provider = BackupProvider::prepare(&ctx0).await.unwrap();
|
||||
// Set up second device.
|
||||
let ctx1 = tcm.unconfigured().await;
|
||||
|
||||
ctx1.evtracker.clear_events();
|
||||
get_backup(&ctx1, provider.qr()).await.unwrap();
|
||||
|
||||
// Make sure the provider finishes without an error.
|
||||
tokio::time::timeout(Duration::from_secs(30), provider)
|
||||
.await
|
||||
.expect("timed out")
|
||||
.expect("error in provider");
|
||||
|
||||
wait_for_item_changed(&ctx1).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync() -> 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 new_name = "new name";
|
||||
alice0
|
||||
.set_config(Config::Displayname, Some(new_name))
|
||||
.await?;
|
||||
alice1.evtracker.clear_events();
|
||||
sync(&alice0, &alice1).await;
|
||||
wait_for_item_changed(&alice1).await;
|
||||
assert_eq!(
|
||||
alice1.get_config(Config::Displayname).await?,
|
||||
Some(new_name.to_owned())
|
||||
);
|
||||
|
||||
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
|
||||
let file = alice0.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
alice0
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
alice1.evtracker.clear_events();
|
||||
sync(&alice0, &alice1).await;
|
||||
wait_for_item_changed(&alice1).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
|
||||
use crate::contact;
|
||||
use crate::contact::Contact;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::Message;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::stock_str;
|
||||
use crate::test_utils::mark_as_verified;
|
||||
use crate::test_utils::TestContext;
|
||||
@@ -394,3 +394,40 @@ async fn test_aeap_replay_attack() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_write_to_alice_after_aeap() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_grp_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?;
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let bob_alice_contact = bob.add_or_lookup_contact(alice).await;
|
||||
assert!(bob_alice_contact.is_verified(bob).await?);
|
||||
let bob_alice_chat = bob.create_chat(alice).await;
|
||||
assert!(bob_alice_chat.is_protected());
|
||||
let bob_unprotected_grp_id = bob
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[alice])
|
||||
.await;
|
||||
|
||||
tcm.change_addr(alice, "alice@someotherdomain.xyz").await;
|
||||
let sent = alice.send_text(alice_grp_id, "Hello!").await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
assert!(!bob_alice_contact.is_verified(bob).await?);
|
||||
let bob_alice_chat = Chat::load_from_db(bob, bob_alice_chat.id).await?;
|
||||
assert!(bob_alice_chat.is_protected());
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg(bob, bob_alice_chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// But encrypted communication is still possible in unprotected groups with old Alice.
|
||||
let sent = bob
|
||||
.send_text(bob_unprotected_grp_id, "Alice, how is your address change?")
|
||||
.await;
|
||||
let msg = Message::load_from_db(bob, sent.sender_msg_id).await?;
|
||||
assert!(msg.get_showpadlock());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
31
src/tools.rs
31
src/tools.rs
@@ -47,20 +47,27 @@ use crate::stock_str;
|
||||
/// end of the shortened string.
|
||||
pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let count = buf.chars().count();
|
||||
if count > approx_chars + DC_ELLIPSIS.len() {
|
||||
let end_pos = buf
|
||||
.char_indices()
|
||||
.nth(approx_chars)
|
||||
.map(|(n, _)| n)
|
||||
.unwrap_or_default();
|
||||
if count <= approx_chars + DC_ELLIPSIS.len() {
|
||||
return Cow::Borrowed(buf);
|
||||
}
|
||||
let end_pos = buf
|
||||
.char_indices()
|
||||
.nth(approx_chars)
|
||||
.map(|(n, _)| n)
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(index) = buf[..end_pos].rfind([' ', '\n']) {
|
||||
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSIS))
|
||||
} else {
|
||||
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSIS))
|
||||
}
|
||||
if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) {
|
||||
Cow::Owned(format!(
|
||||
"{}{}",
|
||||
&buf.get(..=index).unwrap_or_default(),
|
||||
DC_ELLIPSIS
|
||||
))
|
||||
} else {
|
||||
Cow::Borrowed(buf)
|
||||
Cow::Owned(format!(
|
||||
"{}{}",
|
||||
&buf.get(..end_pos).unwrap_or_default(),
|
||||
DC_ELLIPSIS
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
636
src/webxdc.rs
636
src/webxdc.rs
File diff suppressed because it is too large
Load Diff
@@ -57,14 +57,34 @@ impl Context {
|
||||
}
|
||||
|
||||
// Check if a Webxdc shall be used as an integration and remember that.
|
||||
pub(crate) async fn update_webxdc_integration_database(&self, msg: &Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Webxdc && msg.param.get_int(Param::WebxdcIntegration).is_some()
|
||||
{
|
||||
self.set_config_internal(
|
||||
Config::WebxdcIntegration,
|
||||
Some(&msg.id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
pub(crate) async fn update_webxdc_integration_database(
|
||||
&self,
|
||||
msg: &mut Message,
|
||||
context: &Context,
|
||||
) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
let is_integration = if msg.param.get_int(Param::WebxdcIntegration).is_some() {
|
||||
true
|
||||
} else if msg.chat_id.is_self_talk(context).await? {
|
||||
let info = msg.get_webxdc_info(context).await?;
|
||||
if info.request_integration == "map" {
|
||||
msg.param.set_int(Param::WebxdcIntegration, 1);
|
||||
msg.update_param(context).await?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_integration {
|
||||
self.set_config_internal(
|
||||
Config::WebxdcIntegration,
|
||||
Some(&msg.id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -101,11 +121,26 @@ impl Message {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the message is an actually used as Webxdc integration.
|
||||
pub(crate) async fn is_set_as_webxdc_integration(&self, context: &Context) -> Result<bool> {
|
||||
if let Some(integration_id) = context
|
||||
.get_config_parsed::<u32>(Config::WebxdcIntegration)
|
||||
.await?
|
||||
{
|
||||
Ok(integration_id == self.id.to_u32())
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::test_utils::TestContext;
|
||||
use anyhow::Result;
|
||||
use std::time::Duration;
|
||||
@@ -126,4 +161,65 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_overwrite_default_integration() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let self_chat = &t.get_self_chat().await;
|
||||
assert!(t.init_webxdc_integration(None).await?.is_none());
|
||||
|
||||
async fn assert_integration(t: &Context, name: &str) -> Result<()> {
|
||||
let integration_id = t.init_webxdc_integration(None).await?.unwrap();
|
||||
let integration = Message::load_from_db(t, integration_id).await?;
|
||||
let integration_info = integration.get_webxdc_info(t).await?;
|
||||
assert_eq!(integration_info.name, name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// set default integration
|
||||
let bytes = include_bytes!("../../test-data/webxdc/with-manifest-and-png-icon.xdc");
|
||||
let file = t.get_blobdir().join("maps.xdc");
|
||||
tokio::fs::write(&file, bytes).await.unwrap();
|
||||
t.set_webxdc_integration(file.to_str().unwrap()).await?;
|
||||
assert_integration(&t, "with some icon").await?;
|
||||
|
||||
// send a maps.xdc with insufficient manifest
|
||||
let mut msg = Message::new(Viewtype::Webxdc);
|
||||
msg.set_file_from_bytes(
|
||||
&t,
|
||||
"mapstest.xdc",
|
||||
include_bytes!("../../test-data/webxdc/mapstest-integration-unset.xdc"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
t.send_msg(self_chat.id, &mut msg).await;
|
||||
assert_integration(&t, "with some icon").await?; // still the default integration
|
||||
|
||||
// send a maps.xdc with manifest including the line `request_integration = "map"`
|
||||
let mut msg = Message::new(Viewtype::Webxdc);
|
||||
msg.set_file_from_bytes(
|
||||
&t,
|
||||
"mapstest.xdc",
|
||||
include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let sent = t.send_msg(self_chat.id, &mut msg).await;
|
||||
let info = msg.get_webxdc_info(&t).await?;
|
||||
assert!(info.summary.contains("Used as map"));
|
||||
assert_integration(&t, "Maps Test 2").await?;
|
||||
|
||||
// when maps.xdc is received on another device, the integration is not accepted (needs to be forwarded again)
|
||||
let t2 = TestContext::new_alice().await;
|
||||
let msg2 = t2.recv_msg(&sent).await;
|
||||
let info = msg2.get_webxdc_info(&t2).await?;
|
||||
assert!(info.summary.contains("To use as map,"));
|
||||
assert!(t2.init_webxdc_integration(None).await?.is_none());
|
||||
|
||||
// deleting maps.xdc removes the user's integration - the UI will go back to default calling set_webxdc_integration() then
|
||||
message::delete_msgs(&t, &[msg.id]).await?;
|
||||
assert!(t.init_webxdc_integration(None).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +146,11 @@ pub(crate) async fn intercept_get_updates(
|
||||
item: StatusUpdateItem {
|
||||
payload: serde_json::to_value(location_item)?,
|
||||
info: None,
|
||||
href: None,
|
||||
document: None,
|
||||
summary: None,
|
||||
uid: None,
|
||||
notify: None,
|
||||
},
|
||||
serial: StatusUpdateSerial(location.location_id),
|
||||
max_serial: StatusUpdateSerial(location.location_id),
|
||||
@@ -179,7 +181,7 @@ mod tests {
|
||||
async fn test_maps_integration() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let bytes = include_bytes!("../../test-data/webxdc/mapstest.xdc");
|
||||
let bytes = include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc");
|
||||
let file = t.get_blobdir().join("maps.xdc");
|
||||
tokio::fs::write(&file, bytes).await.unwrap();
|
||||
t.set_webxdc_integration(file.to_str().unwrap()).await?;
|
||||
@@ -197,13 +199,12 @@ mod tests {
|
||||
|
||||
let integration = Message::load_from_db(&t, integration_id).await?;
|
||||
let info = integration.get_webxdc_info(&t).await?;
|
||||
assert_eq!(info.name, "Maps Test");
|
||||
assert_eq!(info.name, "Maps Test 2");
|
||||
assert_eq!(info.internet_access, true);
|
||||
|
||||
t.send_webxdc_status_update(
|
||||
integration_id,
|
||||
r#"{"payload": {"action": "pos", "lat": 11.0, "lng": 12.0, "label": "poi #1"}}"#,
|
||||
"descr",
|
||||
)
|
||||
.await?;
|
||||
t.evtracker
|
||||
@@ -237,7 +238,6 @@ mod tests {
|
||||
t.send_webxdc_status_update(
|
||||
integration_id,
|
||||
r#"{"payload": {"action": "pos", "lat": 22.0, "lng": 23.0, "label": "poi #2"}}"#,
|
||||
"descr",
|
||||
)
|
||||
.await?;
|
||||
let updates = t
|
||||
|
||||
BIN
test-data/webxdc/mapstest-integration-set.xdc
Normal file
BIN
test-data/webxdc/mapstest-integration-set.xdc
Normal file
Binary file not shown.
Reference in New Issue
Block a user