Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
189a093e02 feat: compress backups with gzip 2024-10-25 01:15:55 +00:00
127 changed files with 3932 additions and 6358 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.83.0
RUSTUP_TOOLCHAIN: 1.82.0
steps:
- uses: actions/checkout@v4
with:
@@ -37,10 +37,8 @@ jobs:
run: cargo fmt --all -- --check
- name: Run clippy
run: scripts/clippy.sh
- name: Check with all features
- name: Check
run: cargo check --workspace --all-targets --all-features
- name: Check with only default features
run: cargo check --all-targets
npm_constants:
name: Check if node constants are up to date
@@ -97,11 +95,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.83.0
rust: 1.82.0
- os: windows-latest
rust: 1.83.0
rust: 1.82.0
- os: macos-latest
rust: 1.83.0
rust: 1.82.0
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest
@@ -251,7 +249,7 @@ jobs:
- name: Run python tests
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
@@ -316,6 +314,6 @@ jobs:
- name: Run deltachat-rpc-client tests
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
working-directory: deltachat-rpc-client
run: tox -e py

View File

@@ -33,7 +33,7 @@ jobs:
working-directory: deltachat-jsonrpc/typescript
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
- name: make sure websocket server version still builds
working-directory: deltachat-jsonrpc
run: cargo build --bin deltachat-jsonrpc-server --features webserver

View File

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

View File

@@ -64,5 +64,5 @@ jobs:
working-directory: node
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"

View File

@@ -1,288 +1,5 @@
# Changelog
## [1.151.4] - 2024-12-03
### Features / Changes
- Encrypt notification tokens.
### Fixes
- Replace connectivity state "Connected" with "Preparing".
### Miscellaneous Tasks
- Beta clippy suggestions ([#6271](https://github.com/deltachat/deltachat-core-rust/pull/6271)).
### Tests
- Fix `cargo check` for `receive_emails` benchmark.
### CI
- Also run cargo check without all-features.
## [1.151.3] - 2024-12-02
### API-Changes
- Remove experimental `request_internet_access` option from webxdc's `manifest.toml`.
- Add getWebxdcHref to json api ([#6281](https://github.com/deltachat/deltachat-core-rust/pull/6281)).
### CI
- Update Rust to 1.83.0.
### Documentation
- Update dc_msg_get_info_type() and dc_get_securejoin_qr() ([#6269](https://github.com/deltachat/deltachat-core-rust/pull/6269)).
- Fix references to iroh-related headers in peer_channels docs.
- Improve CFFI docs, link to corresponding JSON-RPC docs.
### Features / Changes
- Allow the user to replace maps integration ([#5678](https://github.com/deltachat/deltachat-core-rust/pull/5678)).
- Mark saved messages chat as protected.
### Fixes
- Close iroh endpoint when I/O is stopped.
- Do not add protection messages to Saved Messages chat.
- Mark Saved Messages chat as protected if it exists.
- Sync chat action even if sync message arrives before first one from contact ([#6259](https://github.com/deltachat/deltachat-core-rust/pull/6259)).
### Refactor
- Remove some .unwrap() calls.
- Create_status_update_record: Remove double check of info_msg_id.
- Use Option::or_else() to dedup emitting IncomingWebxdcNotify.
## [1.151.2] - 2024-11-26
### API-Changes
- Deprecate webxdc `descr` parameter ([#6255](https://github.com/deltachat/deltachat-core-rust/pull/6255)).
### Features / Changes
- AEAP: Check that the old peerstate verified key fingerprint hasn't changed when removing it.
- Add `AccountsChanged` and `AccountsItemChanged` events ([#6118](https://github.com/deltachat/deltachat-core-rust/pull/6118)).
- Do not use format=flowed in outgoing messages ([#6256](https://github.com/deltachat/deltachat-core-rust/pull/6256)).
- Add webxdc limits api.
- Add href to IncomingWebxdcNotify event ([#6266](https://github.com/deltachat/deltachat-core-rust/pull/6266)).
### Fixes
- Revert treating some transient SMTP errors as permanent.
### Refactor
- Create_status_update_record: Get rid of `notify` var.
### Tests
- Check that IncomingMsg isn't emitted for reactions.
## [1.151.1] - 2024-11-24
### Build system
- nix: Fix deltachat-rpc-server-source installable.
### CI
- Test building nix targets to avoid regressions.
## [1.151.0] - 2024-11-23
### Features / Changes
- Trim whitespace from scanned QR codes.
- Use privacy-preserving webxdc addresses ([#6237](https://github.com/deltachat/deltachat-core-rust/pull/6237)).
- Webxdc notify ([#6230](https://github.com/deltachat/deltachat-core-rust/pull/6230)).
- `update.href` api ([#6248](https://github.com/deltachat/deltachat-core-rust/pull/6248)).
### Fixes
- Never notify SELF ([#6251](https://github.com/deltachat/deltachat-core-rust/pull/6251)).
### Build system
- Use underscores in deltachat-rpc-server source package filename.
- Remove imap_tools from dependencies ([#6238](https://github.com/deltachat/deltachat-core-rust/pull/6238)).
- cargo: Update Rustls from 0.23.14 to 0.23.18.
- deps: Bump curve25519-dalek from 3.2.0 to 4.1.3 in /fuzz.
### Documentation
- Move style guide into a separate document.
- Clarify DC_EVENT_INCOMING_WEBXDC_NOTIFY documentation ([#6249](https://github.com/deltachat/deltachat-core-rust/pull/6249)).
### Tests
- After AEAP, 1:1 chat isn't available for sending, but unprotected groups are ([#6222](https://github.com/deltachat/deltachat-core-rust/pull/6222)).
## [1.150.0] - 2024-11-21
### API-Changes
- Correct `DC_CERTCK_ACCEPT_*` values and docs ([#6176](https://github.com/deltachat/deltachat-core-rust/pull/6176)).
### Features / Changes
- Use Rustls for connections with strict TLS ([#6186](https://github.com/deltachat/deltachat-core-rust/pull/6186)).
- Experimental header protection for Autocrypt.
- Tune down io-not-started info in connectivity-html.
- Clear config cache in start_io() ([#6228](https://github.com/deltachat/deltachat-core-rust/pull/6228)).
- Line-before-quote may be up to 120 character long instead of 80.
- Use i.delta.chat in qr codes ([#6223](https://github.com/deltachat/deltachat-core-rust/pull/6223)).
### Fixes
- Prevent accidental wrong-password-notifications ([#6122](https://github.com/deltachat/deltachat-core-rust/pull/6122)).
- Remove footers from "Show Full Message...".
- `send_msg_to_smtp`: Return Ok if `smtp` row is deleted in parallel.
- Only add "member added/removed" messages if they actually do that ([#5992](https://github.com/deltachat/deltachat-core-rust/pull/5992)).
- Do not fail to load chatlist summary if the message got removed.
- deltachat-jsonrpc: Do not fail `get_chatlist_items_by_entries` if the message got deleted.
- deltachat-jsonrpc: Do not fail `get_draft` if draft is deleted.
- `markseen_msgs`: Limit not yet downloaded messages state to `InNoticed` ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)).
- Update state of message when fully downloading it.
- Dont overwrite equal drafts ([#6212](https://github.com/deltachat/deltachat-core-rust/pull/6212)).
### Build system
- Silence RUSTSEC-2024-0384.
- cargo: Update rPGP from 0.13.2 to 0.14.0.
- cargo: Update futures-concurrency from 7.6.1 to 7.6.2.
- Update flake.nix ([#6200](https://github.com/deltachat/deltachat-core-rust/pull/6200))
### CI
- Ensure flake is formatted.
### Documentation
- Scanned proxies are added and normalized.
### Refactor
- Fix nightly clippy warnings.
- Remove slicing from `is_file_in_use`.
- Remove unnecessary `allow(clippy::indexing_slicing)`.
- Don't use slicing in `remove_nonstandard_footer`.
- Do not use slicing in `qr` module.
- Eliminate indexing in `compute_mailinglist_name`.
- Remove unused `allow(clippy::indexing_slicing)`.
- Remove indexing/slicing from `remove_message_footer`.
- Remove indexing/slicing from `squash_attachment_parts`.
- Remove unused allow(clippy::indexing_slicing) for heuristically_parse_ndn.
- Remove indexing/slicing from `parse_message_ids`.
- Remove slicing from `remove_bottom_quote`.
- Get rid of slicing in `remove_top_quote`.
- Remove unused allow(clippy::indexing_slicing) from 'truncate'.
- Forbid clippy::indexing_slicing.
- Forbid clippy::string_slice.
- Delete chat in a transaction.
- Fix typo in `context.rs`.
### Tests
- Remove all calls to print() from deltachat-rpc-client tests.
- Reply to protected group from MUA.
- Mark not downloaded message as seen ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)).
- Mark `receive_imf()` as only for tests and "internals" feature ([#6235](https://github.com/deltachat/deltachat-core-rust/pull/6235)).
## [1.149.0] - 2024-11-05
### Build system
- Update tokio to 1.41 and Android NDK to r27.
- `nix flake update android`.
### Fixes
- cargo: Update iroh to 0.28.1.
This fixes the problem with iroh not sending the `Host:` header and not being able to connect to relays behind nginx reverse proxy.
## [1.148.7] - 2024-11-03
### API-Changes
- Add API to reset contact encryption.
### Features / Changes
- Emit chatlist events only if message still exists.
### Fixes
- send_msg_to_smtp: Do not fail if the message does not exist anymore.
- Do not percent-encode dot when passing to autoconfig server.
- Save contact name from SecureJoin QR to `authname`, not to `name` ([#6115](https://github.com/deltachat/deltachat-core-rust/pull/6115)).
- Always exit fake IDLE after at most 60 seconds.
- Concat NDNs ([#6129](https://github.com/deltachat/deltachat-core-rust/pull/6129)).
### Refactor
- Remove `has_decrypted_pgp_armor()`.
### Miscellaneous Tasks
- Update dependencies.
## [1.148.6] - 2024-10-31
### API-Changes
- Add Message::new_text() ([#6123](https://github.com/deltachat/deltachat-core-rust/pull/6123)).
- Add `MessageSearchResult.chat_id` ([#6120](https://github.com/deltachat/deltachat-core-rust/pull/6120)).
### Features / Changes
- Enable Webxdc realtime by default ([#6125](https://github.com/deltachat/deltachat-core-rust/pull/6125)).
### Fixes
- Save full text to mime_headers for long outgoing messages ([#6091](https://github.com/deltachat/deltachat-core-rust/pull/6091)).
- Show root SMTP connection failure in connectivity view ([#6121](https://github.com/deltachat/deltachat-core-rust/pull/6121)).
- Skip IDLE if we got unsolicited FETCH ([#6130](https://github.com/deltachat/deltachat-core-rust/pull/6130)).
### Miscellaneous Tasks
- Silence another rust-analyzer false-positive ([#6124](https://github.com/deltachat/deltachat-core-rust/pull/6124)).
- cargo: Upgrade iroh to 0.26.0.
### Refactor
- Directly use connectives ([#6128](https://github.com/deltachat/deltachat-core-rust/pull/6128)).
- Use Message::new_text() more ([#6127](https://github.com/deltachat/deltachat-core-rust/pull/6127)).
## [1.148.5] - 2024-10-27
### Fixes
- Set Config::NotifyAboutWrongPw before saving configuration ([#5896](https://github.com/deltachat/deltachat-core-rust/pull/5896)).
- Do not take write lock for maybe_network_lost() and set_push_device_token().
- Do not lock the account manager for the whole duration of background_fetch.
### Features / Changes
- Auto-restore 1:1 chat protection after receiving old unverified message.
### CI
- Take `CHATMAIL_DOMAIN` from variables instead of secrets.
### Other
- Revert "build: nix flake update fenix" to fix `nix build .#deltachat-rpc-server-armeabi-v7a-android`.
### Refactor
- Receive_imf::add_parts: Remove excessive `from_id == ContactId::SELF` checks.
- Factor out `add_gossip_peer_from_header()`.
## [1.148.4] - 2024-10-24
### Features / Changes
@@ -429,7 +146,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
- Reset quota on configured address change ([#5908](https://github.com/deltachat/deltachat-core-rust/pull/5908)).
- Do not emit progress 1000 when configuration is cancelled.
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/deltachat/deltachat-core-rust/pull/5338)).
- Re-add tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
- Readd tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
### Miscellaneous Tasks
@@ -1020,7 +737,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
### Tests
- deltachat-rpc-client: re-enable `log_cli`.
- deltachat-rpc-client: reenable `log_cli`.
## [1.140.0] - 2024-06-04
@@ -1957,7 +1674,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
- Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol.
- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat.
- deltachat-repl: Enable INFO logging by default and add timestamps.
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elements based on the configuration key which is a part of the event.
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elments based on the configuration key which is a part of the event.
- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)).
- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)).
- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)).
@@ -4674,10 +4391,14 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
- new qr-code type `DC_QR_WEBRTC` #1779
- new `dc_chatlist_get_summary2()` api #1771
- tweak smtp-timeout for larger mails #1782
- optimize read-receipts #1765
- Allow http scheme for DCACCOUNT URLs #1770
- improve tests #1769
- bug fixes #1766 #1772 #1773 #1775 #1776 #1777
@@ -5418,13 +5139,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.148.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.1..v1.148.2
[1.148.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.2..v1.148.3
[1.148.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.3..v1.148.4
[1.148.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.4..v1.148.5
[1.148.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.5..v1.148.6
[1.148.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.6..v1.148.7
[1.149.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.7..v1.149.0
[1.150.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.149.0..v1.150.0
[1.151.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.150.0..v1.151.0
[1.151.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.0..v1.151.1
[1.151.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.1..v1.151.2
[1.151.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.2..v1.151.3
[1.151.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.3..v1.151.4

View File

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

1522
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.151.4"
version = "1.148.4"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -42,11 +42,12 @@ anyhow = { workspace = true }
async-broadcast = "0.7.1"
async-channel = { workspace = true }
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
async-compression = { version = "0.4.15", default-features = false, features = ["tokio", "gzip"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
brotli = { version = "7", default-features=false, features = ["std"] }
brotli = { version = "6", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
@@ -61,12 +62,11 @@ hickory-resolver = "=0.25.0-alpha.2"
http-body-util = "0.1.2"
humansize = "2"
hyper = "1"
hyper-util = "0.1.10"
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.29", default-features = false, features = ["net"] }
iroh = { version = "0.29", default-features = false }
iroh-base = { version = "0.29", features = ["base32"] }
kamadak-exif = "0.6.1"
hyper-util = "0.1.9"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.25.0", default-features = false, features = ["net"] }
iroh-net = { version = "0.25.0", default-features = false }
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
mailparse = "0.15"
@@ -77,23 +77,22 @@ num-traits = { workspace = true }
once_cell = { workspace = true }
parking_lot = "0.12"
percent-encoding = "2.3"
pgp = { version = "0.14.0", default-features = false }
pgp = { version = "0.13.2", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.37"
quick-xml = "0.36"
quoted_printable = "0.5"
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.10.0"
rustls = { version = "0.23.19", default-features = false }
rustls-pki-types = "1.9.0"
rustls = { version = "0.23.13", 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"
@@ -110,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.7"
webpki-roots = "0.26.6"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
@@ -123,7 +122,6 @@ proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = { workspace = true }
testdir = "0.9.0"
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tracing-subscriber.workspace = true
[workspace]
members = [
@@ -152,7 +150,6 @@ harness = false
[[bench]]
name = "receive_emails"
required-features = ["internals"]
harness = false
[[bench]]
@@ -175,22 +172,29 @@ chrono = { version = "0.4.38", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
futures = "0.3.31"
futures-lite = "2.5.0"
futures = "0.3.30"
futures-lite = "2.3.0"
libc = "0.2"
log = "0.4"
nu-ansi-term = "0.46"
num-traits = "0.2"
once_cell = "1.20.2"
once_cell = "1.18.0"
rand = "0.8"
regex = "1.10"
rusqlite = "0.32"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.14.0"
tempfile = "3.13.0"
thiserror = "1"
tokio = "1"
# 1.38 is the latest version before `mio` dependency update
# that broke compilation with Android NDK r23c and r24.
# Version 1.39.0 cannot be compiled using these NDKs,
# see issue <https://github.com/tokio-rs/tokio/issues/6748>
# for details.
tokio = "~1.38.1"
tokio-util = "0.7.11"
tracing-subscriber = "0.3"
yerpc = "0.6.2"

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

@@ -418,7 +418,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts
* default=send and request read receipts, only send but not request if `bot` is set
* default=send and request read receipts, only send but not reuqest if `bot` is set
* - `bcc_self` = 0=do not send a copy of outgoing messages to self,
* 1=send a copy of outgoing messages to self (default).
* Sending messages to self is needed for a proper multi-account setup,
@@ -506,11 +506,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
* This is an experimental option not compatible to other MUAs
* and older Delta Chat versions.
* 1 = enable.
* 0 = disable (default).
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
@@ -535,8 +530,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
* however, are not handled by the core otherwise.
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop.
* 1 = WebXDC realtime API is enabled (default).
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
* 1 = WebXDC realtime API is enabled.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -985,7 +980,7 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co
* If the increation-method is not used - which is probably the normal case -
* dc_send_msg() copies the file to the blob directory if it is not yet there.
* To distinguish the two cases, msg->state must be set properly. The easiest
* way to ensure this is to reuse the same object for both calls.
* way to ensure this is to re-use the same object for both calls.
*
* Example:
* ~~~
@@ -1154,14 +1149,9 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the message with the webxdc instance.
* @param json program-readable data, this is created in JS land as:
* - `payload`: any JS object or primitive.
* - `info`: optional informational message. Will be shown in chat and may be added as system notification.
* note that also users that are not notified explicitly get the `info` or `summary` update shown in the chat.
* - `document`: optional document name. shown eg. in title bar.
* - `summary`: optional summary. shown beside app icon.
* - `notify`: optional array of other users `selfAddr` to be notified e.g. by a sound about `info` or `summary`.
* @param descr Deprecated, set to NULL
* @param json program-readable data, the actual payload
* @param descr The user-visible description of JSON data,
* in case of a chess game, e.g. the move.
* @return 1=success, 0=error
*/
int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const char* json, const char* descr);
@@ -2546,8 +2536,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_PROXY with dc_lot_t::text1=address:
* ask the user if they want to use the given proxy.
* if so, call dc_set_config_from_qr() and restart I/O.
* On success, dc_get_config(context, "proxy_url")
* will contain the new proxy in normalized form as the first element.
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* e-mail address scanned, optionally, a draft message could be set in
@@ -2597,15 +2585,13 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* The returned text will also work as a normal https:-link,
* so that the QR code is useful also without Delta Chat being installed
* or can be passed to contacts through other channels.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id If set to a group-chat-id,
@@ -4201,13 +4187,9 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
* - internet_access:
* true if the Webxdc should get internet access;
* this is the case i.e. for experimental maps integration.
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
* Should be exposed to `webxdc.sendUpdateMaxSize` in JS land.
* true if the Webxdc should get full internet access, including Webrtc.
* currently, this is only true for encrypted Webxdc's in the self chat
* that have requested internet access in the manifest.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
@@ -4493,7 +4475,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
*
* Even when you display an icon,
* you should still display the text of the informational message using dc_msg_get_text()
@@ -4523,24 +4504,6 @@ 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().
@@ -4706,7 +4669,7 @@ int dc_msg_has_html (dc_msg_t* msg);
* If the download fails or succeeds,
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
*
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any further download action.
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any futher download action.
* It was fully downloaded, but we failed to decrypt it.
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
*
@@ -5741,14 +5704,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_CERTCK_STRICT 1
/**
* Accept certificates that are expired, self-signed
* or not valid for the server hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID 2
/**
* For API compatibility only: Treat this as DC_CERTCK_ACCEPT_INVALID on reading.
* Must not be written.
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID_CERTIFICATES 3
@@ -5788,23 +5745,6 @@ void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
* returns immediately and once the result is ready it can be retrieved via dc_jsonrpc_next_response()
* the jsonrpc specification defines an invocation id that can then be used to match request and response.
*
* An overview of JSON-RPC calls is available at
* <https://js.jsonrpc.delta.chat/classes/RawClient.html>.
* Note that the page describes only the rough methods.
* Calling convention, casing etc. does vary, this is a known flaw,
* and at some point we will get to improve that :)
*
* Also, note that most calls are more high-level than this CFFI, require more database calls and are slower.
* They're more suitable for an environment that is totally async and/or cannot use CFFI, which might not be true for native apps.
*
* Notable exceptions that exist only as JSON-RPC and probably never get a CFFI counterpart:
* - getMessageReactions(), sendReaction()
* - getHttpResponse()
* - draftSelfReport()
* - getAccountFileSize()
* - importVcard(), parseVcard(), makeVcard()
* - sendWebxdcRealtimeData, sendWebxdcRealtimeAdvertisement(), leaveWebxdcRealtime()
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param request JSON-RPC request as string
@@ -5825,8 +5765,6 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Make a JSON-RPC call and return a response.
*
* See dc_jsonrpc_request() for an overview of possible calls and for more information.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param input JSON-RPC request.
@@ -5922,26 +5860,15 @@ int dc_event_get_data2_int(dc_event_t* event);
/**
* Get data associated with an event object.
* The meaning of the data depends on the event ID returned as @ref DC_EVENT constants.
* The meaning of the data depends on the event ID
* returned as @ref DC_EVENT constants by dc_event_get_id().
* See also dc_event_get_data1_int() and dc_event_get_data2_int().
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data1" string or NULL.
* The meaning depends on the event type associated with this event.
* Must be freed using dc_str_unref().
*/
char* dc_event_get_data1_str(dc_event_t* event);
/**
* Get data associated with an event object.
* The meaning of the data depends on the event ID returned as @ref DC_EVENT constants.
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data2" string or NULL.
* The meaning depends on the event type associated with this event.
* Must be freed using dc_str_unref().
* @return "data2" as a string or NULL.
* the meaning depends on the event type associated with this event.
* Once you're done with the string, you have to unref it using dc_unref_str().
*/
char* dc_event_get_data2_str(dc_event_t* event);
@@ -6139,35 +6066,12 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_INCOMING_REACTION 2002
/**
* A webxdc wants an info message or a changed summary to be notified.
*
* @param data1 (int) contact_id ID _and_ (char*) href.
* - dc_event_get_data1_int() returns contact_id of the sending contact.
* - dc_event_get_data1_str() returns the href as set to `update.href`.
* @param data2 (int) msg_id _and_ (char*) text_to_notify.
* - dc_event_get_data2_int() returns the msg_id,
* referring to the webxdc-info-message, if there is any.
* Sometimes no webxdc-info-message is added to the chat
* and yet a notification is sent; in this case the msg_id
* of the webxdc instance is returned.
* - dc_event_get_data2_str() returns text_to_notify,
* the text that shall be shown in the notification.
* string must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_INCOMING_WEBXDC_NOTIFY 2003
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
*
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
*
* If the message is a webxdc info message,
* dc_msg_get_parent() returns the webxdc instance the notification belongs to.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
*/
@@ -6451,25 +6355,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
/**
* Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
*
* This event is only emitted by the account manager.
*/
#define DC_EVENT_ACCOUNTS_CHANGED 2302
/**
* Inform that an account property that might be shown in the account list changed, namely:
* - is_configured (see dc_is_configured())
* - displayname
* - selfavatar
* - private_tag
*
* This event is emitted from the account whose property changed.
*/
#define DC_EVENT_ACCOUNTS_ITEM_CHANGED 2303
/**
* Inform that some events have been skipped due to event channel overflow.

View File

@@ -542,7 +542,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingReaction { .. } => 2002,
EventType::IncomingWebxdcNotify { .. } => 2003,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
@@ -569,8 +568,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::AccountsBackgroundFetchDone => 2200,
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
EventType::AccountsChanged => 2302,
EventType::AccountsItemChanged => 2303,
EventType::EventChannelOverflow { .. } => 2400,
#[allow(unreachable_patterns)]
#[cfg(test)]
@@ -603,12 +600,9 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ConfigSynced { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged => 0,
EventType::IncomingReaction { contact_id, .. }
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
| EventType::AccountsBackgroundFetchDone => 0,
EventType::ChatlistChanged => 0,
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -680,8 +674,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::ChatlistItemChanged { .. }
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::ConfigSynced { .. }
| EventType::ChatModified(_)
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
@@ -689,7 +681,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
| EventType::IncomingWebxdcNotify { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -709,27 +700,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_event_get_data1_str(event: *mut dc_event_t) -> *mut libc::c_char {
if event.is_null() {
eprintln!("ignoring careless call to dc_event_get_data1_str()");
return ptr::null_mut();
}
let event = &(*event).typ;
match event {
EventType::IncomingWebxdcNotify { href, .. } => {
if let Some(href) = href {
href.to_c_string().unwrap_or_default().into_raw()
} else {
ptr::null_mut()
}
}
_ => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut libc::c_char {
if event.is_null() {
@@ -778,8 +748,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
@@ -807,9 +775,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
.to_c_string()
.unwrap_or_default()
.into_raw(),
EventType::IncomingWebxdcNotify { text, .. } => {
text.to_c_string().unwrap_or_default().into_raw()
}
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -1094,7 +1059,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
msg_id: u32,
json: *const libc::c_char,
_descr: *const libc::c_char,
descr: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_webxdc_status_update()");
@@ -1102,10 +1067,14 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
}
let ctx = &*context;
block_on(ctx.send_webxdc_status_update(MsgId::new(msg_id), &to_string_lossy(json)))
.context("Failed to send webxdc update")
.log_err(ctx)
.is_ok() as libc::c_int
block_on(ctx.send_webxdc_status_update(
MsgId::new(msg_id),
&to_string_lossy(json),
&to_string_lossy(descr),
))
.context("Failed to send webxdc update")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -3712,17 +3681,6 @@ 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() {
@@ -4910,7 +4868,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.maybe_network_lost().await });
block_on(async move { accounts.write().await.maybe_network_lost().await });
}
#[no_mangle]
@@ -4924,12 +4882,12 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
}
let accounts = &*accounts;
let background_fetch_future = {
let lock = block_on(accounts.read());
lock.background_fetch(Duration::from_secs(timeout_in_seconds))
};
// At this point account manager is not locked anymore.
block_on(background_fetch_future);
block_on(async move {
let accounts = accounts.read().await;
accounts
.background_fetch(Duration::from_secs(timeout_in_seconds))
.await;
});
1
}
@@ -4947,7 +4905,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
let token = to_string_lossy(token);
block_on(async move {
let accounts = accounts.read().await;
let mut accounts = accounts.write().await;
if let Err(err) = accounts.set_push_device_token(&token).await {
accounts.emit_event(EventType::Error(format!(
"Failed to set notify token: {err:#}."

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.151.4"
version = "1.148.4"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -25,7 +25,7 @@ async-channel = { workspace = true }
futures = { workspace = true }
serde_json = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
typescript-type-def = { version = "0.5.12", features = ["json_value"] }
tokio = { workspace = true }
sanitize-filename = { workspace = true }
walkdir = "2.5.0"

View File

@@ -254,12 +254,11 @@ impl CommandApi {
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
let future = {
let lock = self.accounts.read().await;
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
};
// At this point account manager is not locked anymore.
future.await;
self.accounts
.write()
.await
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
.await;
Ok(())
}
@@ -1135,11 +1134,9 @@ impl CommandApi {
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
let message_object = MessageObject::from_msg_id(&ctx, msg_id)
MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))?
.with_context(|| format!("Message {msg_id} does not exist for account {account_id}"))?;
Ok(message_object)
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
@@ -1163,10 +1160,7 @@ impl CommandApi {
messages.insert(
message_id,
match message_result {
Ok(Some(message)) => MessageLoadResult::Message(message),
Ok(None) => MessageLoadResult::LoadingError {
error: "Message does not exist".to_string(),
},
Ok(message) => MessageLoadResult::Message(message),
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
@@ -1424,15 +1418,6 @@ impl CommandApi {
Ok(())
}
/// Resets contact encryption.
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
contact_id.reset_encryption(&ctx).await?;
Ok(())
}
async fn change_contact_name(
&self,
account_id: u32,
@@ -1767,10 +1752,10 @@ impl CommandApi {
account_id: u32,
instance_msg_id: u32,
update_str: String,
_descr: Option<String>,
description: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str)
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
.await
}
@@ -1829,18 +1814,6 @@ impl CommandApi {
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
}
/// Get href from a WebxdcInfoMessage which might include a hash holding
/// information about a specific position or state in a webxdc app (optional)
async fn get_webxdc_href(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
Ok(message.get_webxdc_href())
}
/// Get blob encoded as base64 from a webxdc message
///
/// path is the path of the file within webxdc archive
@@ -2016,7 +1989,9 @@ impl CommandApi {
async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result<Option<MessageObject>> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
Ok(MessageObject::from_msg_id(&ctx, draft.get_id()).await?)
Ok(Some(
MessageObject::from_msg_id(&ctx, draft.get_id()).await?,
))
} else {
Ok(None)
}
@@ -2142,7 +2117,8 @@ impl CommandApi {
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new_text(text);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
@@ -2185,9 +2161,7 @@ impl CommandApi {
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id)
.await?
.context("Just sent message does not exist")?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
Ok((msg_id.to_u32(), message))
}

View File

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

View File

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

View File

@@ -85,8 +85,6 @@ pub struct MessageObject {
webxdc_info: Option<WebxdcMessageInfo>,
webxdc_href: Option<String>,
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
@@ -114,10 +112,8 @@ enum MessageQuote {
}
impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Option<Self>> {
let Some(message) = Message::load_from_db_optional(context, msg_id).await? else {
return Ok(None);
};
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let sender_contact = Contact::get_by_id(context, message.get_from_id())
.await
@@ -187,7 +183,7 @@ impl MessageObject {
.map(Into::into)
.collect();
let message_object = MessageObject {
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
@@ -243,17 +239,12 @@ impl MessageObject {
file_name: message.get_filename(),
webxdc_info,
// On a WebxdcInfoMessage this might include a hash holding
// information about a specific position or state in a webxdc app
webxdc_href: message.get_webxdc_href(),
download_state,
reactions,
vcard_contact: vcard_contacts.first().cloned(),
};
Ok(Some(message_object))
})
}
}
@@ -499,7 +490,6 @@ pub struct MessageSearchResult {
author_name: String,
author_color: String,
author_id: u32,
chat_id: u32,
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
@@ -539,7 +529,6 @@ impl MessageSearchResult {
author_name,
author_color: color_int_to_hex_string(sender.get_color()),
author_id: sender.id.to_u32(),
chat_id: chat.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,

View File

@@ -6,161 +6,87 @@ use typescript_type_def::TypeDef;
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "kind")]
pub enum QrObject {
/// Ask the user whether to verify the contact.
///
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
AskVerifyContact {
/// ID of the contact.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user whether to join the group.
AskVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// ID of the contact.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
FprOk {
/// Contact ID.
contact_id: u32,
},
/// Scanned fingerprint does not match the last seen fingerprint.
FprMismatch {
/// Contact ID.
contact_id: Option<u32>,
},
/// The scanned QR code contains a fingerprint but no e-mail address.
FprWithoutAddr {
/// Key fingerprint.
fingerprint: String,
},
/// Ask the user if they want to create an account on the given domain.
Account {
/// Server domain name.
domain: String,
},
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Authentication token.
auth_token: String,
/// Iroh node address.
node_addr: String,
},
/// Ask the user if they want to use the given service for video chats.
WebrtcInstance {
domain: String,
instance_pattern: String,
},
/// Ask the user if they want to use the given proxy.
///
/// Note that HTTP(S) URLs without a path
/// and query parameters are treated as HTTP(S) proxy URL.
/// UI may want to still offer to open the URL
/// in the browser if QR code contents
/// starts with `http://` or `https://`
/// and the QR code was not scanned from
/// the proxy configuration screen.
Proxy {
/// Proxy URL.
///
/// This is the URL that is going to be added.
url: String,
/// Host extracted from the URL to display in the UI.
host: String,
/// Port extracted from the URL to display in the UI.
port: u16,
},
/// Contact address is scanned.
///
/// Optionally, a draft message could be provided.
/// Ask the user if they want to start chatting.
Addr {
/// Contact ID.
contact_id: u32,
/// Draft message.
draft: Option<String>,
},
/// URL scanned.
///
/// Ask the user if they want to open a browser or copy the URL to clipboard.
Url { url: String },
/// Text scanned.
///
/// Ask the user if they want to copy the text to clipboard.
Text { text: String },
/// Ask the user if they want to withdraw their own QR code.
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to withdraw their own group invite QR code.
WithdrawVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own QR code.
ReviveVerifyContact {
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own group invite QR code.
ReviveVerifyGroup {
/// Contact ID.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// `dclogin:` scheme parameters.
///
/// Ask the user if they want to login with the email address.
Login { address: String },
Login {
address: String,
},
}
impl From<Qr> for QrObject {
@@ -215,6 +141,7 @@ impl From<Qr> for QrObject {
auth_token,
} => QrObject::Backup2 {
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
auth_token,
},
Qr::WebrtcInstance {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -969,7 +969,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"Arguments <msg-id> <json status update> expected"
);
let msg_id = MsgId::new(arg1.parse()?);
context.send_webxdc_status_update(msg_id, arg2).await?;
context
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
.await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -1002,7 +1004,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
if !arg1.is_empty() {
let mut draft = Message::new_text(arg1.to_string());
let mut draft = Message::new(Viewtype::Text);
draft.set_text(arg1.to_string());
sel_chat
.as_ref()
.unwrap()
@@ -1025,7 +1028,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
!arg1.is_empty(),
"Please specify text to add as device message."
);
let mut msg = Message::new_text(arg1.to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text(arg1.to_string());
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {

View File

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

View File

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

View File

@@ -61,8 +61,6 @@ class EventType(str, Enum):
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
ACCOUNTS_CHANGED = "AccountsChanged"
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"

View File

@@ -36,10 +36,6 @@ class Contact:
"""Delete contact."""
self._rpc.delete_contact(self.account.id, self.id)
def reset_encryption(self) -> None:
"""Reset contact encryption."""
self._rpc.reset_contact_encryption(self.account.id, self.id)
def set_name(self, name: str) -> None:
"""Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name)

View File

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

View File

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

View File

@@ -7,8 +7,8 @@ If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import logging
import os
import sys
import threading
import time
@@ -25,7 +25,9 @@ def path_to_webxdc(request):
def log(msg):
logging.info(msg)
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):

View File

@@ -61,7 +61,7 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
# Setup second device for Alice
# to test observing securejoin protocol.
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
files = list(tmp_path.glob("*.tar.gz"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])

View File

@@ -12,6 +12,7 @@ import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.direct_imap import DirectImap
from deltachat_rpc_client.rpc import JsonRpcError
@@ -56,8 +57,8 @@ def test_acfactory(acfactory) -> None:
if event.progress == 1000: # Success
break
else:
logging.info(event)
logging.info("Successful configuration")
print(event)
print("Successful configuration")
def test_configure_starttls(acfactory) -> None:
@@ -245,7 +246,6 @@ def test_contact(acfactory) -> None:
assert repr(alice_contact_bob)
alice_contact_bob.block()
alice_contact_bob.unblock()
alice_contact_bob.reset_encryption()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()
@@ -379,7 +379,7 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
alice = acfactory.new_configured_account()
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
files = list(tmp_path.glob("*.tar.gz"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
@@ -535,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, direct_imap):
def test_reactions_for_a_reordering_move(acfactory):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
@@ -559,7 +559,7 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap = DirectImap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")
@@ -630,7 +630,7 @@ def test_markseen_contact_request(acfactory, tmp_path):
# Bob sets up a second device.
bob.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
files = list(tmp_path.glob("*.tar.gz"))
bob2 = acfactory.get_unconfigured_account()
bob2.import_backup(files[0])
bob2.start_io()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,9 +11,6 @@ ignore = [
# Unmaintained encoding
"RUSTSEC-2021-0153",
# Unmaintained instant
"RUSTSEC-2024-0384",
]
[bans]
@@ -29,19 +26,13 @@ skip = [
{ name = "event-listener", version = "2.5.3" },
{ name = "event-listener", version = "4.0.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "fiat-crypto", version = "0.1.20" },
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "hostname", version = "0.3.1" },
{ name = "h2", version = "0.3.26" },
{ name = "http-body", version = "0.4.6" },
{ name = "http", version = "0.2.12" },
{ name = "idna", version = "0.5.0" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "netlink-packet-route", version = "0.21.0" },
{ name = "hyper", version = "0.14.28" },
{ name = "nix", version = "0.26.4" },
{ name = "nix", version = "0.27.1" },
{ name = "num_enum_derive", version = "0.5.11" },
{ name = "num_enum", version = "0.5.11" },
{ name = "proc-macro-crate", version = "1.3.1" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
@@ -49,16 +40,9 @@ skip = [
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rtnetlink", version = "0.13.1" },
{ name = "strsim", version = "0.10.0" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "time", version = "<0.3" },
{ name = "tokio-tungstenite", version = "0.21.0" },
{ name = "toml_edit", version = "0.19.15" },
{ name = "tungstenite", version = "0.21.0" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
{ name = "windows_aarch64_msvc", version = "<0.52" },
@@ -71,7 +55,6 @@ skip = [
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
{ name = "winreg", version = "0.50.0" },
]
@@ -88,7 +71,6 @@ allow = [
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
]

111
flake.lock generated
View File

@@ -3,15 +3,15 @@
"android": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1731356359,
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
"lastModified": 1712088936,
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
"type": "github"
},
"original": {
@@ -22,17 +22,18 @@
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"android",
"nixpkgs"
]
},
"locked": {
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"lastModified": 1711099426,
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
"owner": "numtide",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
"type": "github"
},
"original": {
@@ -47,11 +48,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1731393059,
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
"lastModified": 1729578683,
"narHash": "sha256-h0Wmvrkadbyi3IJXFLPi+QyYjCAKDr2xQ6dLxlQ8cXY=",
"owner": "nix-community",
"repo": "fenix",
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
"rev": "d66cda53e8193a878742dcadb5bb75f4df7c3c0a",
"type": "github"
},
"original": {
@@ -65,11 +66,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -83,11 +84,29 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@@ -101,11 +120,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1721727458,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
@@ -116,11 +135,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1730207686,
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=",
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
@@ -131,11 +150,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"type": "github"
},
"original": {
@@ -147,11 +166,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1729256560,
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
"type": "github"
},
"original": {
@@ -163,9 +182,10 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 0,
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
"lastModified": 1711668574,
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
"type": "path"
},
"original": {
@@ -175,11 +195,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1729413321,
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
"type": "github"
},
"original": {
@@ -193,7 +213,7 @@
"inputs": {
"android": "android",
"fenix": "fenix",
"flake-utils": "flake-utils_2",
"flake-utils": "flake-utils_3",
"naersk": "naersk",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_4"
@@ -202,11 +222,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1731342671,
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
"lastModified": 1729533545,
"narHash": "sha256-A/AuEWcGwwjpfBCZqWDNNg5GwYrJduzLvlMe+A7xG5U=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
"rev": "de2ff17bc513807412d7bbaba1d995a774938583",
"type": "github"
},
"original": {
@@ -245,6 +265,21 @@
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -18,9 +18,9 @@
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
inherit (sdkPkgs) ndk-27-0-11902837 cmdline-tools-latest;
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
});
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.0.11902837";
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
rustSrc = nix-filter.lib {
root = ./.;
@@ -257,21 +257,13 @@
androidAttrs = {
armeabi-v7a = {
cc = "armv7a-linux-androideabi21-clang";
cc = "armv7a-linux-androideabi19-clang";
rustTarget = "armv7-linux-androideabi";
};
arm64-v8a = {
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
x86 = {
cc = "i686-linux-android21-clang";
rustTarget = "i686-linux-android";
};
x86_64 = {
cc = "x86_64-linux-android21-clang";
rustTarget = "x86_64-linux-android";
};
};
mkAndroidRustPackage = arch: packageName:
@@ -363,8 +355,6 @@
mkRustPackages "x86_64-linux" //
mkRustPackages "armv7l-linux" //
mkRustPackages "armv6l-linux" //
mkRustPackages "x86_64-darwin" //
mkRustPackages "aarch64-darwin" //
mkAndroidPackages "armeabi-v7a" //
mkAndroidPackages "arm64-v8a" //
mkAndroidPackages "x86" //
@@ -481,8 +471,8 @@
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat_rpc_server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-${manifest.version}.tar.gz $out'';
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-client =
@@ -535,30 +525,28 @@
};
};
devShells.default =
let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in
pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
devShells.default = let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
};
}
);
}

View File

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

2483
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1899,7 +1899,7 @@ def test_connectivity(acfactory, lp):
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
@@ -2209,19 +2209,6 @@ def test_configure_error_msgs_wrong_pw(acfactory):
# Password is wrong so it definitely has to say something about "password"
assert "password" in ev.data2
ac1.stop_io()
ac1.set_config("mail_pw", "abc") # Wrong mail pw
ac1.configure()
while True:
ev = ac1._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
print(f"Configuration progress: {ev.data1}")
if ev.data1 == 0:
break
assert "password" in ev.data2
# Account will continue to work with the old password, so if it becomes wrong, a notification
# must be shown.
assert ac1.get_config("notify_about_wrong_pw") == "1"
def test_configure_error_msgs_invalid_server(acfactory):
ac2 = acfactory.get_unconfigured_account()

View File

@@ -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("https://i.delta.chat")
assert qr.startswith("OPENPGP4FPR:")
res = ac2.check_qr(qr)
assert res.is_ask_verifycontact()
assert not res.is_ask_verifygroup()

View File

@@ -1 +1 @@
2024-12-03
2024-10-24

View File

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

View File

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

View File

@@ -5,8 +5,7 @@ use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use futures::future::join_all;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
@@ -139,7 +138,6 @@ impl Accounts {
ctx.open("".to_string()).await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
Ok(account_config.id)
}
@@ -157,7 +155,6 @@ impl Accounts {
.build()
.await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
Ok(account_config.id)
}
@@ -192,7 +189,6 @@ impl Accounts {
.context("failed to remove account data")?;
}
self.config.remove_account(id).await?;
self.emit_event(EventType::AccountsChanged);
Ok(())
}
@@ -305,48 +301,20 @@ impl Accounts {
///
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
async fn background_fetch_without_timeout(&self) {
async fn background_fetch_and_log_error(account: Context) {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
}
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Starting background fetch for {} accounts.",
accounts.len()
)),
});
let mut futures_unordered: FuturesUnordered<_> = accounts
.into_iter()
.map(background_fetch_and_log_error)
.collect();
while futures_unordered.next().await.is_some() {}
}
/// Auxiliary function for [Accounts::background_fetch].
async fn background_fetch_with_timeout(
accounts: Vec<Context>,
events: Events,
timeout: std::time::Duration,
) {
if let Err(_err) = tokio::time::timeout(
timeout,
Self::background_fetch_no_timeout(accounts, events.clone()),
join_all(
self.accounts
.values()
.cloned()
.map(background_fetch_and_log_error),
)
.await
{
events.emit(Event {
id: 0,
typ: EventType::Warning("Background fetch timed out.".to_string()),
});
}
events.emit(Event {
id: 0,
typ: EventType::AccountsBackgroundFetchDone,
});
.await;
}
/// Performs a background fetch for all accounts in parallel with a timeout.
@@ -354,13 +322,15 @@ impl Accounts {
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
/// process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
///
/// Returns a future that resolves when background fetch is done,
/// but does not capture `&self`.
pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
let events = self.events.clone();
Self::background_fetch_with_timeout(accounts, events, timeout)
pub async fn background_fetch(&self, timeout: std::time::Duration) {
if let Err(_err) =
tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await
{
self.emit_event(EventType::Warning(
"Background fetch timed out.".to_string(),
));
}
self.emit_event(EventType::AccountsBackgroundFetchDone);
}
/// Emits a single event.
@@ -374,7 +344,7 @@ impl Accounts {
}
/// Sets notification token for Apple Push Notification service.
pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
self.push_subscriber.set_device_token(token).await;
Ok(())
}

View File

@@ -260,6 +260,7 @@ fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str>
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use tokio::fs;
use tokio::io::AsyncReadExt;
@@ -519,13 +520,8 @@ Authentication-Results: dkim=";
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
// Test that Autocrypt works with mailing list.
//
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
// This is not needed: comparing of the From address to Autocrypt header address is enough.
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -537,18 +533,28 @@ Authentication-Results: dkim=";
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_none());
// Do the same without the mailing list header, this time the peerstate should be accepted
let sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_some());
// Bob can now write encrypted to Alice:
// This also means that Bob can now write encrypted to Alice:
let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await;
assert!(sent.load_from_db().await.get_showpadlock());
// But if Bob writes to a mailing list, Alice doesn't show a padlock
// since she can't verify the signature without accepting Bob's key:
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(rcvd.get_showpadlock());
assert!(!rcvd.get_showpadlock());
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(())

View File

@@ -567,14 +567,6 @@ impl ChatId {
contact_id: Option<ContactId>,
timestamp_sort: i64,
) -> Result<()> {
if contact_id == Some(ContactId::SELF) {
// Do not add protection messages to Saved Messages chat.
// This chat never gets protected and unprotected,
// we do not want the first message
// to be a protection message with an arbitrary timestamp.
return Ok(());
}
let text = context.stock_protection_msg(protect, contact_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
@@ -773,19 +765,27 @@ impl ChatId {
);
let chat = Chat::load_from_db(context, self).await?;
context
.sql
.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
(self,),
)
.await?;
context
.sql
.transaction(|transaction| {
transaction.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
(self,),
)?;
transaction.execute("DELETE FROM msgs WHERE chat_id=?", (self,))?;
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (self,))?;
transaction.execute("DELETE FROM chats WHERE id=?", (self,))?;
Ok(())
})
.execute("DELETE FROM msgs WHERE chat_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM chats WHERE id=?;", (self,))
.await?;
context.emit_msgs_changed_without_ids();
@@ -797,7 +797,8 @@ impl ChatId {
context.scheduler.interrupt_inbox().await;
if chat.is_self_talk() {
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::self_deleted_msg_body(context).await;
add_device_msg(context, None, Some(&mut msg)).await?;
}
chatlist_events::emit_chatlist_changed(context);
@@ -922,27 +923,24 @@ impl ChatId {
&& old_draft.chat_id == self
&& old_draft.state == MessageState::OutDraft
{
let affected_rows = context
.sql.execute(
"UPDATE msgs
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
WHERE id=?7
AND (type <> ?2
OR txt <> ?3
OR txt_normalized <> ?4
OR param <> ?5
OR mime_in_reply_to <> ?6);",
(
time(),
msg.viewtype,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
),
).await?;
return Ok(affected_rows > 0);
context
.sql
.execute(
"UPDATE msgs
SET timestamp=?,type=?,txt=?,txt_normalized=?,param=?,mime_in_reply_to=?
WHERE id=?;",
(
time(),
msg.viewtype,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
),
)
.await?;
return Ok(true);
}
}
}
@@ -1399,7 +1397,7 @@ impl ChatId {
///
/// `message_timestamp` should be either the message "sent" timestamp or a timestamp of the
/// corresponding event in case of a system message (usually the current system time).
/// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
/// `always_sort_to_bottom` makes this ajust the returned timestamp up so that the message goes
/// to the chat bottom.
/// `received` -- whether the message is received. Otherwise being sent.
/// `incoming` -- whether the message is incoming.
@@ -2094,31 +2092,28 @@ impl Chat {
EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
};
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
let new_mime_headers = if msg.has_html() {
if msg.param.exists(Param::Forwarded) {
let html = if msg.param.exists(Param::Forwarded) {
msg.get_id().get_html(context).await?
} else {
msg.param.get(Param::SendHtml).map(|s| s.to_string())
};
match html {
Some(html) => Some(tokio::task::block_in_place(move || {
buf_compress(new_html_mimepart(html).build().as_string().as_bytes())
})?),
None => None,
}
} else {
None
};
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
true => Some(msg.text.clone()),
false => None,
});
let new_mime_headers = match new_mime_headers {
Some(h) => Some(tokio::task::block_in_place(move || {
buf_compress(new_html_mimepart(h).build().as_string().as_bytes())
})?),
None => None,
};
msg.chat_id = self.id;
msg.from_id = ContactId::SELF;
msg.rfc724_mid = new_rfc724_mid;
msg.timestamp_sort = timestamp;
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
let mime_modified = new_mime_headers.is_some() | was_truncated;
// add message to the database
if let Some(update_msg_id) = update_msg_id {
@@ -2147,7 +2142,7 @@ impl Chat {
msg.hidden,
msg.in_reply_to.as_deref().unwrap_or_default(),
new_references,
new_mime_headers.is_some(),
mime_modified,
new_mime_headers.unwrap_or_default(),
location_id as i32,
ephemeral_timer,
@@ -2198,7 +2193,7 @@ impl Chat {
msg.hidden,
msg.in_reply_to.as_deref().unwrap_or_default(),
new_references,
new_mime_headers.is_some(),
mime_modified,
new_mime_headers.unwrap_or_default(),
location_id as i32,
ephemeral_timer,
@@ -2210,9 +2205,7 @@ impl Chat {
msg.id = MsgId::new(u32::try_from(raw_id)?);
maybe_set_logging_xdc(context, msg, self.id).await?;
context
.update_webxdc_integration_database(msg, context)
.await?;
context.update_webxdc_integration_database(msg).await?;
}
context.scheduler.interrupt_ephemeral_task().await;
Ok(msg.id)
@@ -2601,12 +2594,10 @@ impl ChatIdBlocked {
_ => (),
}
let protected = contact_id == ContactId::SELF || {
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
peerstate.is_some_and(|p| {
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
})
};
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let protected = peerstate.map_or(false, |p| {
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
});
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
@@ -2989,8 +2980,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
recipients.push(from);
}
// Default Webxdc integrations are hidden messages and must not be sent out
if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden {
// Webxdc integrations are messages, however, shipped with main app and must not be sent out
if msg.param.get_int(Param::WebxdcIntegration).is_some() {
recipients.clear();
}
@@ -3112,7 +3103,8 @@ pub async fn send_text_msg(
chat_id
);
let mut msg = Message::new_text(text_to_send);
let mut msg = Message::new(Viewtype::Text);
msg.text = text_to_send;
send_msg(context, chat_id, &mut msg).await
}
@@ -4495,7 +4487,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
.await?;
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
// Insert labels for welcome messages to avoid them being readded on reconfiguration.
context
.sql
.execute(
@@ -4512,7 +4504,6 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
/// Adds an informational message to chat.
///
/// For example, it can be a message showing that a member was added to a group.
/// Doesn't fail if the chat doesn't exist.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add_info_msg_with_cmd(
context: &Context,
@@ -4657,9 +4648,9 @@ impl Context {
Contact::create_ex(self, Nosync, to, addr).await?;
return Ok(());
}
let addr = ContactAddress::new(addr).context("Invalid address")?;
let (contact_id, _) =
Contact::add_or_lookup(self, "", &addr, Origin::Hidden).await?;
let contact_id = Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None)
.await?
.with_context(|| format!("No contact for addr '{addr}'"))?;
match action {
SyncAction::Block => {
return contact::set_blocked(self, Nosync, contact_id, true).await
@@ -4669,10 +4660,9 @@ impl Context {
}
_ => (),
}
// Use `Request` so that even if the program crashes, the user doesn't have to look
// into the blocked contacts.
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
ChatIdBlocked::lookup_by_contact(self, contact_id)
.await?
.with_context(|| format!("No chat for addr '{addr}'"))?
.id
}
SyncId::Grpid(grpid) => {
@@ -4709,7 +4699,7 @@ impl Context {
/// Emits the appropriate `MsgsChanged` event. Should be called if the number of unnoticed
/// archived chats could decrease. In general we don't want to make an extra db query to know if
/// a noticed chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
/// a noticied chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
/// is ok.
pub(crate) fn on_archived_chats_maybe_noticed(&self) {
self.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
@@ -4787,7 +4777,8 @@ mod tests {
async fn test_get_draft() {
let t = TestContext::new().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new_text("hello".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
let draft = chat_id.get_draft(&t).await.unwrap().unwrap();
@@ -4801,11 +4792,13 @@ mod tests {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let mut msg = Message::new_text("hi!".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi!".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert!(chat_id.get_draft(&t).await?.is_some());
let mut msg = Message::new_text("another".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("another".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert!(chat_id.get_draft(&t).await?.is_some());
@@ -4819,7 +4812,8 @@ mod tests {
async fn test_forwarding_draft_failing() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new_text("hello".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
@@ -4832,7 +4826,8 @@ mod tests {
async fn test_draft_stable_ids() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
let mut msg = Message::new_text("hello".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
assert_eq!(msg.id, MsgId::new_unset());
assert!(chat_id.get_draft_msg_id(&t).await?.is_none());
@@ -4878,7 +4873,11 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let msgs: Vec<message::Message> = (1..=1000)
.map(|i| Message::new_text(i.to_string()))
.map(|i| {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(i.to_string());
msg
})
.collect();
let mut tasks = Vec::new();
for mut msg in msgs {
@@ -4911,7 +4910,8 @@ mod tests {
.await?;
// save a draft
let mut draft = Message::new_text("draft text".to_string());
let mut draft = Message::new(Viewtype::Text);
draft.set_text("draft text".to_string());
chat_id.set_draft(&t, Some(&mut draft)).await?;
let test = Message::load_from_db(&t, draft.id).await?;
@@ -4964,25 +4964,29 @@ mod tests {
let one2one_msg = Message::load_from_db(&alice, one2one_msg_id).await?;
// quoting messages in same chat is okay
let mut msg = Message::new_text("baz".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&grp_msg)).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_ok());
let mut msg = Message::new_text("baz".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&one2one_msg)).await?;
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
assert!(result.is_ok());
let one2one_quote_reply_msg_id = result.unwrap();
// quoting messages from groups to one-to-ones is okay ("reply privately")
let mut msg = Message::new_text("baz".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&grp_msg)).await?;
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
assert!(result.is_ok());
// quoting messages from one-to-one chats in groups is an error; usually this is also not allowed by UI at all ...
let mut msg = Message::new_text("baz".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&one2one_msg)).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_err());
@@ -5379,7 +5383,7 @@ mod tests {
// Eventually, first removal message arrives.
// This has no effect.
bob.recv_msg_trash(&remove1).await;
bob.recv_msg(&remove1).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())
}
@@ -5474,11 +5478,13 @@ mod tests {
let t = TestContext::new().await;
// add two device-messages
let mut msg1 = Message::new_text("first message".to_string());
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text("first message".to_string());
let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await;
assert!(msg1_id.is_ok());
let mut msg2 = Message::new_text("second message".to_string());
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text("second message".to_string());
let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await;
assert!(msg2_id.is_ok());
assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap());
@@ -5507,12 +5513,14 @@ mod tests {
let t = TestContext::new().await;
// add two device-messages with the same label (second attempt is not added)
let mut msg1 = Message::new_text("first message".to_string());
let mut msg1 = Message::new(Viewtype::Text);
msg1.text = "first message".to_string();
let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await;
assert!(msg1_id.is_ok());
assert!(!msg1_id.as_ref().unwrap().is_unset());
let mut msg2 = Message::new_text("second message".to_string());
let mut msg2 = Message::new(Viewtype::Text);
msg2.text = "second message".to_string();
let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await;
assert!(msg2_id.is_ok());
assert!(msg2_id.as_ref().unwrap().is_unset());
@@ -5559,7 +5567,8 @@ mod tests {
let res = add_device_msg(&t, Some("some-label"), None).await;
assert!(res.is_ok());
let mut msg = Message::new_text("message text".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await;
assert!(msg_id.is_ok());
@@ -5576,7 +5585,8 @@ mod tests {
add_device_msg(&t, Some("some-label"), None).await.ok();
assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap());
let mut msg = Message::new_text("message text".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
add_device_msg(&t, Some("another-label"), Some(&mut msg))
.await
.ok();
@@ -5593,7 +5603,8 @@ mod tests {
async fn test_delete_device_chat() {
let t = TestContext::new().await;
let mut msg = Message::new_text("message text".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.ok();
@@ -5616,7 +5627,8 @@ mod tests {
.await
.unwrap();
let mut msg = Message::new_text("message text".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err());
assert!(prepare_msg(&t, device_chat_id, &mut msg).await.is_err());
@@ -5627,7 +5639,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_and_reset_all_device_msgs() {
let t = TestContext::new().await;
let mut msg = Message::new_text("message text".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("message text".to_string());
let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg))
.await
.unwrap();
@@ -5659,7 +5672,8 @@ mod tests {
async fn test_archive() {
// create two chats
let t = TestContext::new().await;
let mut msg = Message::new_text("foo".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
.await
@@ -5959,7 +5973,8 @@ mod tests {
let t = TestContext::new().await;
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
let mut msg = Message::new_text("foo".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foo".to_string());
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
let chat_id1 = message::Message::load_from_db(&t, msg_id)
.await
@@ -6036,7 +6051,8 @@ mod tests {
ChatVisibility::Pinned,
);
let mut msg = Message::new_text("hi!".into());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi!".into());
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, alice_chat_id);
@@ -6675,7 +6691,8 @@ mod tests {
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
let mut msg = Message::new_text("Hi Bob".to_owned());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Hi Bob".to_owned());
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
let msg = bob.recv_msg(&sent_msg).await;
@@ -6726,7 +6743,8 @@ mod tests {
let received_msg = bob.recv_msg(&sent_msg).await;
// Bob quotes received message and sends a reply to Alice.
let mut reply = Message::new_text("Reply".to_owned());
let mut reply = Message::new(Viewtype::Text);
reply.set_text("Reply".to_owned());
reply.set_quote(&bob, Some(&received_msg)).await?;
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
let received_reply = alice.recv_msg(&sent_reply).await;
@@ -6809,7 +6827,8 @@ mod tests {
let group_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
add_contact_to_chat(&alice, group_id, bob_id).await?;
let mut msg = Message::new_text("bla foo".to_owned());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("bla foo".to_owned());
let sent_msg = alice.send_msg(group_id, &mut msg).await;
assert!(sent_msg.payload().contains("secretgrpname"));
assert!(sent_msg.payload().contains("secretname"));
@@ -7478,71 +7497,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_accept_before_first_msg() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(alice0).await?;
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
assert_eq!(a0b_contact.origin, Origin::CreateChat);
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not);
sync(alice0, alice1).await;
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::CreateChat);
let a1b_chat = alice1.get_chat(&bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Not);
let chats = Chatlist::try_load(alice1, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
assert_eq!(rcvd_msg.chat_id, a1b_chat.id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_block_before_first_msg() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.block(alice0).await?;
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom);
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes);
sync(alice0, alice1).await;
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::Hidden);
assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id)
.await?
.is_none());
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom);
let a1b_chat = alice1.get_chat(&bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Yes);
assert_eq!(rcvd_msg.chat_id, a1b_chat.id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_adhoc_grp() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
@@ -7769,19 +7723,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_do_not_overwrite_draft() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let mut msg = Message::new_text("This is a draft message".to_string());
let self_chat = alice.get_self_chat().await.id;
self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap();
let draft1 = self_chat.get_draft(&alice).await?.unwrap();
SystemTime::shift(Duration::from_secs(1));
self_chat.set_draft(&alice, Some(&mut msg)).await.unwrap();
let draft2 = self_chat.get_draft(&alice).await?.unwrap();
assert_eq!(draft1.timestamp_sort, draft2.timestamp_sort);
Ok(())
}
}

View File

@@ -394,32 +394,25 @@ impl Chatlist {
&chat_loaded
};
let lastmsg = if let Some(lastmsg_id) = lastmsg_id {
// Message may be deleted by the time we try to load it,
// so use `load_from_db_optional` instead of `load_from_db`.
Message::load_from_db_optional(context, lastmsg_id)
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id)
.await
.context("Loading message failed")?
} else {
None
};
let lastcontact = if let Some(lastmsg) = &lastmsg {
.context("loading message failed")?;
if lastmsg.from_id == ContactId::SELF {
None
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
Some(lastcontact)
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single => None,
Chattype::Single => (Some(lastmsg), None),
}
}
} else {
None
(None, None)
};
if chat.id.is_archived_link() {
@@ -483,6 +476,7 @@ mod tests {
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
};
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -516,7 +510,8 @@ mod tests {
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
// 2s here.
for chat_id in &[chat_id1, chat_id3, chat_id2] {
let mut msg = Message::new_text("hello".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
}
@@ -760,7 +755,8 @@ mod tests {
.await
.unwrap();
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foo:\nbar \r\n test".to_string());
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
@@ -768,25 +764,6 @@ mod tests {
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
}
/// Tests that summary does not fail to load
/// if the draft was deleted after loading the chatlist.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_deleted_draft() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let mut msg = Message::new_text("Foobar".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
chat_id.set_draft(&t, None).await.unwrap();
let summary_res = chats.get_summary(&t, 0, None).await;
assert!(summary_res.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;

View File

@@ -80,7 +80,7 @@ pub enum Config {
/// SMTP server security (e.g. TLS, STARTTLS).
SendSecurity,
/// Deprecated option for backwards compatibility.
/// Deprecated option for backwards compatibilty.
///
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
SmtpCertificateChecks,
@@ -396,12 +396,6 @@ pub enum Config {
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Enable header protection for `Autocrypt` header.
///
/// This is an experimental setting not compatible to other MUAs
/// and older Delta Chat versions (core version <= v1.149.0).
ProtectAutocrypt,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]
@@ -439,14 +433,7 @@ pub enum Config {
WebxdcIntegration,
/// Enable webxdc realtime features.
#[strum(props(default = "1"))]
WebxdcRealtimeEnabled,
/// Last device token stored on the chatmail server.
///
/// If it has not changed, we do not store
/// the device token again.
DeviceToken,
}
impl Config {
@@ -797,12 +784,6 @@ impl Context {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
}
if matches!(
key,
Config::Displayname | Config::Selfavatar | Config::PrivateTag
) {
self.emit_event(EventType::AccountsItemChanged);
}
if key.is_synced() {
self.emit_event(EventType::ConfigSynced { key });
}

View File

@@ -19,12 +19,11 @@ use auto_outlook::outlk_autodiscover;
use deltachat_contact_tools::EmailAddress;
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::utf8_percent_encode;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use server_params::{expand_param_vector, ServerParams};
use tokio::task;
use crate::config::{self, Config};
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::LogExt;
@@ -32,14 +31,14 @@ use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
};
use crate::message::Message;
use crate::message::{Message, Viewtype};
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 {
@@ -112,10 +111,15 @@ impl Context {
let param = EnteredLoginParam::load(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let configured_param = configure(self, &param).await?;
let configured_param_res = configure(self, &param).await;
self.set_config_internal(Config::NotifyAboutWrongPw, None)
.await?;
on_configure_completed(self, configured_param_res?, old_addr).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, configured_param, old_addr).await?;
Ok(())
}
}
@@ -143,7 +147,8 @@ async fn on_configure_completed(
}
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new_text(provider.after_login_hint.to_string());
let mut msg = Message::new(Viewtype::Text);
msg.text = provider.after_login_hint.to_string();
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
@@ -156,9 +161,9 @@ async fn on_configure_completed(
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Some(old_addr) = old_addr {
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new_text(
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
);
let mut msg = Message::new(Viewtype::Text);
msg.text =
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await;
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
@@ -412,8 +417,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
configured_param.oauth2,
r,
);
let configuring = true;
let mut imap_session = match imap.connect(ctx, configuring).await {
let mut imap_session = match imap.connect(ctx).await {
Ok(session) => session,
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
};
@@ -486,7 +490,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
update_device_chats_handle.await??;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.emit_event(EventType::AccountsItemChanged);
Ok(configured_param)
}
@@ -500,15 +503,7 @@ async fn get_autoconfig(
param: &EnteredLoginParam,
param_domain: &str,
) -> Option<Vec<ServerParams>> {
// Make sure to not encode `.` as `%2E` here.
// Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML
// when address is encoded.
// E.g.
// <https://autoconfig.murena.io/mail/config-v1.1.xml?emailaddress=foobar%40example%2Eorg>
// produced XML file with `<username>foobar@example%2Eorg</username>`
// resulting in failure to log in.
let param_addr_urlencoded =
utf8_percent_encode(&param.addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
if let Ok(res) = moz_autoconfigure(
ctx,
@@ -612,6 +607,8 @@ pub enum Error {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::login_param::EnteredServerLoginParam;

View File

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

View File

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

View File

@@ -143,43 +143,6 @@ impl ContactId {
.await?;
Ok(())
}
/// Returns contact address.
pub async fn addr(&self, context: &Context) -> Result<String> {
let addr = context
.sql
.query_row("SELECT addr FROM contacts WHERE id=?", (self,), |row| {
let addr: String = row.get(0)?;
Ok(addr)
})
.await?;
Ok(addr)
}
/// Resets encryption with the contact.
///
/// Effect is similar to receiving a message without Autocrypt header
/// from the contact, but this action is triggered manually by the user.
///
/// For example, this will result in sending the next message
/// to 1:1 chat unencrypted, but will not remove existing verified keys.
pub async fn reset_encryption(self, context: &Context) -> Result<()> {
let now = time();
let addr = self.addr(context).await?;
if let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? {
peerstate.degrade_encryption(now);
peerstate.save_to_db(&context.sql).await?;
}
// Reset 1:1 chat protection.
if let Some(chat_id) = ChatId::lookup_by_contact(context, self).await? {
chat_id
.set_protection(context, ProtectionStatus::Unprotected, now, Some(self))
.await?;
}
Ok(())
}
}
impl fmt::Display for ContactId {
@@ -462,12 +425,9 @@ pub enum Origin {
/// To: of incoming messages of unknown sender
IncomingUnknownTo = 0x40,
/// Address scanned but not verified.
/// address scanned but not verified
UnhandledQrScan = 0x80,
/// Address scanned from a SecureJoin QR code, but not verified yet.
UnhandledSecurejoinQrScan = 0x81,
/// Reply-To: of incoming message of known sender
/// Contacts with at least this origin value are shown in the contact list.
IncomingReplyTo = 0x100,
@@ -1252,15 +1212,15 @@ impl Contact {
let fingerprint_self = load_self_public_key(context)
.await?
.dc_fingerprint()
.fingerprint()
.to_string();
let fingerprint_other_verified = peerstate
.peek_key(true)
.map(|k| k.dc_fingerprint().to_string())
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(false)
.map(|k| k.dc_fingerprint().to_string())
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
if addr < peerstate.addr {
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
@@ -3189,75 +3149,4 @@ Until the false-positive is fixed:
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reset_encryption() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
assert_eq!(msg.get_showpadlock(), false);
let msg = tcm.send_recv(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reset_verified_encryption() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.execute_securejoin(bob, alice).await;
let msg = tcm.send_recv(bob, alice, "Encrypted").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
// Check that the contact is still verified after resetting encryption.
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?;
assert_eq!(alice_bob_contact.is_verified(alice).await?, true);
// 1:1 chat and profile is no longer verified.
assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false);
let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await;
assert_eq!(
info_msg.text,
"bob@example.net sent a message from another device."
);
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_is_verified() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
assert_eq!(contact.is_verified(&alice).await?, true);
assert!(contact.is_profile_verified(&alice).await?);
assert!(contact.get_verifier_id(&alice).await?.is_none());
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
Ok(())
}
}

View File

@@ -10,10 +10,9 @@ use std::time::Duration;
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use pgp::types::PublicKeyTrait;
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use tokio::sync::{Mutex, Notify, OnceCell, RwLock};
use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
@@ -29,7 +28,7 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::peerstate::Peerstate;
@@ -276,7 +275,7 @@ pub struct InnerContext {
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: parking_lot::RwLock<String>,
pub(crate) last_error: std::sync::RwLock<String>,
/// If debug logging is enabled, this contains all necessary information
///
@@ -292,7 +291,7 @@ pub struct InnerContext {
pub(crate) push_subscribed: AtomicBool,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
pub(crate) iroh: OnceCell<Iroh>,
}
/// The state of ongoing process.
@@ -446,11 +445,11 @@ impl Context {
metadata: RwLock::new(None),
creation_time: tools::Time::now(),
last_full_folder_scan: Mutex::new(None),
last_error: parking_lot::RwLock::new("".to_string()),
last_error: std::sync::RwLock::new("".to_string()),
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
iroh: Arc::new(RwLock::new(None)),
iroh: OnceCell::new(),
};
let ctx = Context {
@@ -472,33 +471,12 @@ impl Context {
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
// The next line is mainly for iOS:
// iOS starts a separate process for receiving notifications and if the user concurrently
// starts the app, the UI process opens the database but waits with calling start_io()
// until the notifications process finishes.
// Now, some configs may have changed, so, we need to invalidate the cache.
self.sql.config_cache.write().await.clear();
self.scheduler.start(self.clone()).await;
}
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
self.scheduler.stop(self).await;
if let Some(iroh) = self.iroh.write().await.take() {
// Close all QUIC connections.
// Spawn into a separate task,
// because Iroh calls `wait_idle()` internally
// and it may take time, especially if the network
// has become unavailable.
tokio::spawn(async move {
// We do not log the error because we do not want the task
// to hold the reference to Context.
let _ = tokio::time::timeout(Duration::from_secs(60), iroh.close()).await;
});
}
}
/// Restarts the IO scheduler if it was running before
@@ -509,7 +487,7 @@ impl Context {
/// Indicate that the network likely has come back.
pub async fn maybe_network(&self) {
if let Some(ref iroh) = *self.iroh.read().await {
if let Some(iroh) = self.iroh.get() {
iroh.network_change().await;
}
self.scheduler.maybe_network().await;
@@ -803,7 +781,7 @@ impl Context {
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
let fingerprint_str = match load_self_public_key(self).await {
Ok(key) => key.dc_fingerprint().hex(),
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {err}>"),
};
@@ -1012,12 +990,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"protect_autocrypt",
self.get_config_int(Config::ProtectAutocrypt)
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
@@ -1199,14 +1171,15 @@ impl Context {
EncryptPreference::Mutual,
&public_key,
);
let fingerprint = public_key.dc_fingerprint();
let fingerprint = public_key.fingerprint();
peerstate.set_verified(public_key, fingerprint, "".to_string())?;
peerstate.save_to_db(&self.sql).await?;
chat_id
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
.await?;
let mut msg = Message::new_text(self.get_self_report().await?);
let mut msg = Message::new(Viewtype::Text);
msg.text = self.get_self_report().await?;
chat_id.set_draft(self, Some(&mut msg)).await?;
@@ -1771,7 +1744,6 @@ mod tests {
"socks5_password",
"key_id",
"webxdc_integration",
"device_token",
];
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();
@@ -1806,10 +1778,12 @@ mod tests {
assert!(res.is_empty());
// Add messages to chat with Bob.
let mut msg1 = Message::new_text("foobar".to_string());
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text("foobar".to_string());
send_msg(&alice, chat.id, &mut msg1).await?;
let mut msg2 = Message::new_text("barbaz".to_string());
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text("barbaz".to_string());
send_msg(&alice, chat.id, &mut msg2).await?;
alice.send_text(chat.id, "Δ-Chat").await;
@@ -1912,7 +1886,8 @@ mod tests {
.await;
// Add 999 messages
let mut msg = Message::new_text("foobar".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foobar".to_string());
for _ in 0..999 {
send_msg(&alice, chat.id, &mut msg).await?;
}
@@ -2086,41 +2061,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
let alice = TestContext::new_alice().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Change the config circumventing the cache
// This simulates what the notification plugin on iOS might do
// because it runs in a different process
alice
.sql
.execute(
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
(),
)
.await?;
// Alice's Delta Chat doesn't know about it yet:
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Starting IO will fail of course because no server settings are configured,
// but it should invalidate the caches:
alice.start_io().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("0".to_string())
);
Ok(())
}
}

View File

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

View File

@@ -1,36 +1,125 @@
//! End-to-end decryption support.
use std::collections::HashSet;
use std::str::FromStr;
use anyhow::Result;
use deltachat_contact_tools::addr_cmp;
use mailparse::ParsedMail;
use crate::aheader::Aheader;
use crate::authres::handle_authres;
use crate::authres::{self, DkimResults};
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::peerstate::Peerstate;
use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
///
/// If successful and the message is encrypted, returns decrypted body.
/// If successful and the message is encrypted, returns decrypted body and a set of valid
/// signature fingerprints.
///
/// If the message is wrongly signed, HashSet will be empty.
pub fn try_decrypt(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
) -> Result<Option<::pgp::composed::Message>> {
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};
let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::pk_decrypt(data, private_keyring)?;
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
}
Ok(Some(msg))
pub(crate) async fn prepare_decryption(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DecryptionInfo> {
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
info!(
context,
"Ignoring autocrypt header since this is a mailing list message. \
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
);
}
return Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header: None,
peerstate: None,
message_time,
dkim_results: DkimResults { dkim_passed: false },
});
}
let autocrypt_header = if context.is_self_addr(from).await? {
None
} else if let Some(aheader_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
} else {
None
};
let dkim_results = handle_authres(context, mail, from).await?;
let allow_aeap = get_encrypted_mime(mail).is_some();
let peerstate = get_autocrypt_peerstate(
context,
from,
autocrypt_header.as_ref(),
message_time,
allow_aeap,
)
.await?;
Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header,
peerstate,
message_time,
dkim_results,
})
}
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
pub from: String,
pub autocrypt_header: Option<Aheader>,
/// The peerstate that will be used to validate the signatures
pub peerstate: Option<Peerstate>,
/// The timestamp when the message was sent.
/// If this is older than the peerstate's last_seen, this probably
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
pub(crate) dkim_results: authres::DkimResults,
}
/// Returns a reference to the encrypted payload of a message.
pub(crate) fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
@@ -115,6 +204,37 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
}
}
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let data = mail.get_body_raw()?;
if has_decrypted_pgp_armor(&data) {
let (plain, ret_valid_signatures) =
pgp::pk_decrypt(data, private_keyring, public_keyring_for_validate)?;
return Ok(Some((plain, ret_valid_signatures)));
}
Ok(None)
}
#[allow(clippy::indexing_slicing)]
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
if let Some(index) = input.iter().position(|b| *b > b' ') {
if input.len() - index > 26 {
let start = index;
let end = start + 27;
return &input[start..end] == b"-----BEGIN PGP MESSAGE-----";
}
}
false
}
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
///
/// Returns the signed part and the set of key
@@ -182,7 +302,7 @@ pub(crate) async fn get_autocrypt_peerstate(
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.dc_fingerprint(),
&header.public_key.fingerprint(),
from,
)
.await?;
@@ -226,6 +346,24 @@ mod tests {
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" \n-----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" -----BEGIN PGP MESSAGE---";
assert_eq!(has_decrypted_pgp_armor(data), false);
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b"blas";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted

View File

@@ -318,7 +318,8 @@ mod tests {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
let mut msg = Message::new_text("Hi Bob".to_owned());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Hi Bob".to_owned());
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
@@ -440,7 +441,7 @@ mod tests {
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
alice
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#)
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#, "d")
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;

View File

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

View File

@@ -223,9 +223,8 @@ impl ChatId {
self.inner_set_ephemeral_timer(context, timer).await?;
if self.is_promoted(context).await? {
let mut msg = Message::new_text(
stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await,
);
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
@@ -1363,7 +1362,8 @@ mod tests {
chat.id
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
.await?;
let mut msg = Message::new_text("hi".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi".to_string());
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
.await
.is_err());
@@ -1393,7 +1393,8 @@ mod tests {
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
let mut poi_msg = Message::new_text("Here".to_string());
let mut poi_msg = Message::new(Viewtype::Text);
poi_msg.text = "Here".to_string();
poi_msg.set_location(10.0, 20.0);
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;

View File

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

View File

@@ -144,12 +144,12 @@ impl HtmlMsgParser {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().eq_ignore_ascii_case("flowed")
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().eq_ignore_ascii_case("yes")
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
},
@@ -283,6 +283,7 @@ mod tests {
<meta name="color-scheme" content="light dark" />
</head><body>
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"#
);
@@ -301,6 +302,7 @@ This message does not have Content-Type nor Subject.<br/>
<meta name="color-scheme" content="light dark" />
</head><body>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<br/>
</body></html>
"#
);
@@ -323,6 +325,7 @@ This line ends with a space and will be merged with the next one due to format=f
<br/>
This line does not end with a space<br/>
and will be wrapped as usual.<br/>
<br/>
</body></html>
"#
);
@@ -344,6 +347,7 @@ mime-modified should not be set set as there is no html and no special stuff;<br
although not being a delta-message.<br/>
test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/>
<br/>
<br/>
</body></html>
"#
);
@@ -521,7 +525,8 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// alice sends a message with html-part to bob
let chat_id = alice.create_chat(&bob).await.id;
let mut msg = Message::new_text("plain text".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("plain text".to_string());
msg.set_html(Some("<b>html</b> text".to_string()));
assert!(msg.mime_modified);
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();

View File

@@ -36,12 +36,11 @@ use crate::log::LogExt;
use crate::login_param::{
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::oauth2::get_oauth2_access_token;
use crate::push::encrypt_device_token;
use crate::receive_imf::{
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
};
@@ -90,7 +89,7 @@ pub(crate) struct Imap {
oauth2: bool,
authentication_failed_once: bool,
login_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
@@ -255,7 +254,7 @@ impl Imap {
proxy_config,
strict_tls,
oauth2,
authentication_failed_once: false,
login_failed_once: false,
connectivity: Default::default(),
conn_last_try: UNIX_EPOCH,
conn_backoff_ms: 0,
@@ -291,11 +290,7 @@ impl Imap {
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection
/// parameters.
pub(crate) async fn connect(
&mut self,
context: &Context,
configuring: bool,
) -> Result<Session> {
pub(crate) async fn connect(&mut self, context: &Context) -> Result<Session> {
let now = tools::Time::now();
let until_can_send = max(
min(self.conn_last_try, now)
@@ -403,12 +398,12 @@ impl Imap {
let mut lock = context.server_id.write().await;
lock.clone_from(&session.capabilities.server_id);
self.authentication_failed_once = false;
self.login_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
lp.user
)));
self.connectivity.set_preparing(context).await;
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
return Ok(session);
}
@@ -417,38 +412,37 @@ impl Imap {
let imap_user = lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
let err_str = err.to_string();
warn!(context, "IMAP failed to login: {err:#}.");
first_error.get_or_insert(format_err!("{message} ({err:#})"));
// If it looks like the password is wrong, send a notification:
let _lock = context.wrong_pw_warning_mutex.lock().await;
if err.to_string().to_lowercase().contains("authentication") {
if self.authentication_failed_once
&& !configuring
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
let mut msg = Message::new_text(message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& err_str.to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
{
warn!(context, "Failed to add device message: {e:#}.");
} else {
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
.log_err(context)
.ok();
}
} else {
self.authentication_failed_once = true;
{
warn!(context, "{e:#}.");
}
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text.clone_from(&message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
.await
{
warn!(context, "Failed to add device message: {e:#}.");
}
} else {
self.authentication_failed_once = false;
self.login_failed_once = true;
}
}
}
@@ -462,8 +456,7 @@ impl Imap {
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined.
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
let configuring = false;
let mut session = match self.connect(context, configuring).await {
let mut session = match self.connect(context).await {
Ok(session) => session,
Err(err) => {
self.connectivity.set_err(context, &err).await;
@@ -1203,8 +1196,6 @@ impl Session {
.await
.context("failed to fetch flags")?;
let mut got_unsolicited_fetch = false;
while let Some(fetch) = list
.try_next()
.await
@@ -1214,7 +1205,6 @@ impl Session {
uid
} else {
info!(context, "FETCH result contains no UID, skipping");
got_unsolicited_fetch = true;
continue;
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
@@ -1237,15 +1227,6 @@ impl Session {
warn!(context, "FETCH result contains no MODSEQ");
}
}
drop(list);
if got_unsolicited_fetch {
// We got unsolicited FETCH, which means some flags
// have been modified while our request was in progress.
// We may or may not have these new flags as a part of the response,
// so better skip next IDLE and do another round of flag synchronization.
self.new_mail = true;
}
set_modseq(context, folder, highest_modseq)
.await
@@ -1560,53 +1541,17 @@ impl Session {
return Ok(());
};
let device_token_changed = context
.get_config(Config::DeviceToken)
.await?
.map_or(true, |config_token| device_token != config_token);
if device_token_changed && self.can_metadata() && self.can_push() {
if self.can_metadata() && self.can_push() {
let folder = context
.get_config(Config::ConfiguredInboxFolder)
.await?
.context("INBOX is not configured")?;
let encrypted_device_token =
encrypt_device_token(&device_token).context("Failed to encrypt device token")?;
// We expect that the server supporting `XDELTAPUSH` capability
// has non-synchronizing literals support as well:
// <https://www.rfc-editor.org/rfc/rfc7888>.
let encrypted_device_token_len = encrypted_device_token.len();
if encrypted_device_token_len <= 4096 {
self.run_command_and_check_ok(&format_setmetadata(
&folder,
&encrypted_device_token,
))
.await
.context("SETMETADATA command failed")?;
// Store device token saved on the server
// to prevent storing duplicate tokens.
// The server cannot deduplicate on its own
// because encryption gives a different
// result each time.
context
.set_config_internal(Config::DeviceToken, Some(&device_token))
.await?;
} else {
// If Apple or Google (FCM) gives us a very large token,
// do not even try to give it to IMAP servers.
//
// Limit of 4096 is arbitrarily selected
// to be the same as required by LITERAL- IMAP extension.
//
// Dovecot supports LITERAL+ and non-synchronizing literals
// of any length, but there is no reason for tokens
// to be that large even after OpenPGP encryption.
warn!(context, "Device token is too long for LITERAL-, ignoring.");
}
self.run_command_and_check_ok(format!(
"SETMETADATA \"{folder}\" (/private/devicetoken \"{device_token}\")"
))
.await
.context("SETMETADATA command failed")?;
context.push_subscribed.store(true, Ordering::Relaxed);
} else if !context.push_subscriber.heartbeat_subscribed().await {
let context = context.clone();
@@ -1618,13 +1563,6 @@ impl Session {
}
}
fn format_setmetadata(folder: &str, device_token: &str) -> String {
let device_token_len = device_token.len();
format!(
"SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
)
}
impl Session {
/// Returns success if we successfully set the flag or we otherwise
/// think add_flag should not be retried: Disconnection during setting
@@ -1774,21 +1712,17 @@ impl Imap {
}
impl Session {
/// Return whether the server sent an unsolicited EXISTS or FETCH response.
///
/// Return whether the server sent an unsolicited EXISTS response.
/// Drains all responses from `session.unsolicited_responses` in the process.
///
/// If this returns `true`, this means that new emails arrived
/// or flags have been changed.
/// In this case we may want to skip next IDLE and do a round
/// of fetching new messages and synchronizing seen flags.
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
/// If this returns `true`, this means that new emails arrived and you should
/// fetch again, even if you just fetched.
fn server_sent_unsolicited_exists(&self, context: &Context) -> Result<bool> {
use async_imap::imap_proto::Response;
use async_imap::imap_proto::ResponseCode;
use UnsolicitedResponse::*;
let folder = self.selected_folder.as_deref().unwrap_or_default();
let mut should_refetch = false;
let mut unsolicited_exists = false;
while let Ok(response) = self.unsolicited_responses.try_recv() {
match response {
Exists(_) => {
@@ -1796,38 +1730,28 @@ impl Session {
context,
"Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
);
should_refetch = true;
unsolicited_exists = true;
}
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them
Expunge(_) | Recent(_) => {}
Other(ref response_data) => {
match response_data.parsed() {
Response::Fetch { .. } => {
info!(
context,
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
);
should_refetch = true;
}
Other(response_data)
if matches!(
response_data.parsed(),
Response::Fetch { .. }
| Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
}
) => {}
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them.
Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
} => {}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
}
}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
}
}
Ok(should_refetch)
Ok(unsolicited_exists)
}
}
@@ -1960,7 +1884,7 @@ async fn needs_move_to_mvbox(
&& has_chat_version
&& headers
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
.filter(|val| val.to_ascii_lowercase() == "auto-generated")
.is_some()
{
if let Some(from) = mimeparser::get_from(headers) {
@@ -2908,16 +2832,4 @@ mod tests {
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
);
}
#[test]
fn test_setmetadata_device_token() {
assert_eq!(
format_setmetadata("INBOX", "foobarbaz"),
"SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)"
);
assert_eq!(
format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"),
"SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)"
);
}
}

View File

@@ -9,6 +9,7 @@ use tokio::time::timeout;
use super::session::Session;
use super::Imap;
use crate::context::Context;
use crate::imap::FolderMeaning;
use crate::net::TIMEOUT;
use crate::tools::{self, time_elapsed};
@@ -31,7 +32,7 @@ impl Session {
self.select_with_uidvalidity(context, folder).await?;
if self.drain_unsolicited_responses(context)? {
if self.server_sent_unsolicited_exists(context)? {
self.new_mail = true;
}
@@ -108,16 +109,37 @@ impl Imap {
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
session: &mut Session,
watch_folder: String,
folder_meaning: FolderMeaning,
) -> Result<()> {
let fake_idle_start_time = tools::Time::now();
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// Wait for 60 seconds or until we are interrupted.
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
Err(_) => info!(context, "Fake IDLE finished."),
Ok(_) => info!(context, "Fake IDLE interrupted."),
// Loop until we are interrupted or until we fetch something.
loop {
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
Err(_) => {
// Let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
let res = self
.fetch_new_messages(context, session, &watch_folder, folder_meaning, false)
.await?;
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break;
}
}
Ok(_) => {
info!(context, "Fake IDLE interrupted.");
break;
}
}
}
info!(

View File

@@ -66,11 +66,21 @@ impl Imap {
&& folder_meaning != FolderMeaning::Drafts
&& folder_meaning != FolderMeaning::Trash
{
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
// Drain leftover unsolicited EXISTS messages
session.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !session.server_sent_unsolicited_exists(context)? {
break;
}
}
}
}

View File

@@ -4,14 +4,14 @@ use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use ::pgp::types::PublicKeyTrait;
use ::pgp::types::KeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use futures::TryStreamExt;
use futures_lite::FutureExt;
use pin_project::pin_project;
use tokio::fs::{self, File};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf};
use tokio_tar::Archive;
use crate::blob::BlobDirContents;
@@ -123,7 +123,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
let name = dirent.file_name();
let name: String = name.to_string_lossy().into();
if name.starts_with("delta-chat")
&& name.ends_with(".tar")
&& (name.ends_with(".tar") || name.ends_with(".tar.gz"))
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
// We just use string comparison to determine which backup is newer.
@@ -269,30 +269,24 @@ async fn import_backup(
context.get_dbfile().display()
);
import_backup_stream(context, backup_file, file_size, passphrase).await?;
let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
if backup_to_import.extension() == Some(OsStr::new("gz")) {
let backup_file = tokio::io::BufReader::new(backup_file);
let backup_file = async_compression::tokio::bufread::GzipDecoder::new(backup_file);
import_backup_stream(context, backup_file, passphrase).await?;
} else {
import_backup_stream(context, backup_file, passphrase).await?;
}
Ok(())
}
/// Imports backup by reading a tar file from a stream.
///
/// `file_size` is used to calculate the progress
/// and emit progress events.
/// Ideally it is the sum of the entry
/// sizes without the header overhead,
/// but can be estimated as tar file size
/// in which case the progress is underestimated
/// and may not reach 99.9% by the end of import.
/// Underestimating is better than
/// overestimating because the progress
/// jumps to 100% instead of getting stuck at 99.9%
/// for some time.
pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
context: &Context,
backup_file: R,
file_size: u64,
passphrase: String,
) -> Result<()> {
import_backup_stream_inner(context, backup_file, file_size, passphrase)
import_backup_stream_inner(context, backup_file, passphrase)
.await
.0
}
@@ -319,6 +313,19 @@ struct ProgressReader<R> {
}
impl<R> ProgressReader<R> {
/// Creates a new `ProgressReader`.
///
/// `file_size` is used to calculate the progress
/// and emit progress events.
/// Ideally it is the sum of the entry
/// sizes without the header overhead,
/// but can be estimated as tar file size
/// in which case the progress is underestimated
/// and may not reach 99.9% by the end of import.
/// Underestimating is better than
/// overestimating because the progress
/// jumps to 100% instead of getting stuck at 99.9%
/// for some time.
fn new(r: R, context: Context, file_size: u64) -> Self {
Self {
inner: r,
@@ -358,10 +365,8 @@ where
async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
context: &Context,
backup_file: R,
file_size: u64,
passphrase: String,
) -> (Result<()>,) {
let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
let mut archive = Archive::new(backup_file);
let mut entries = match archive.entries() {
@@ -426,7 +431,6 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
if res.is_ok() {
context.emit_event(EventType::ImexProgress(999));
res = context.sql.run_migrations(context).await;
context.emit_event(EventType::AccountsItemChanged);
}
if res.is_ok() {
delete_and_reset_all_device_msgs(context)
@@ -462,10 +466,10 @@ fn get_next_backup_path(
tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
let mut tempfile = folder.clone();
tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
tempfile.push(format!("{stem}-{i:02}-{addr}.tar.gz.part"));
let mut destfile = folder.clone();
destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
destfile.push(format!("{stem}-{i:02}-{addr}.tar.gz"));
if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
return Ok((tempdbfile, tempfile, destfile));
@@ -505,9 +509,13 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
file_size += blob.to_abs_path().metadata()?.len()
}
export_backup_stream(context, &temp_db_path, blobdir, file, file_size)
.await
.context("Exporting backup to file failed")?;
let gzip_encoder = async_compression::tokio::write::GzipEncoder::new(file);
let mut gzip_encoder =
export_backup_stream(context, &temp_db_path, blobdir, gzip_encoder, file_size)
.await
.context("Exporting backup to file failed")?;
gzip_encoder.shutdown().await?;
fs::rename(temp_path, &dest_path).await?;
context.emit_event(EventType::ImexFileWritten(dest_path));
Ok(())
@@ -544,6 +552,10 @@ impl<W> ProgressWriter<W> {
context,
}
}
fn into_inner(self) -> W {
self.inner
}
}
impl<W> AsyncWrite for ProgressWriter<W>
@@ -591,12 +603,12 @@ pub(crate) async fn export_backup_stream<'a, W>(
blobdir: BlobDirContents<'a>,
writer: W,
file_size: u64,
) -> Result<()>
) -> Result<W>
where
W: tokio::io::AsyncWrite + tokio::io::AsyncWriteExt + Unpin + Send + 'static,
{
let writer = ProgressWriter::new(writer, context.clone(), file_size);
let mut builder = tokio_tar::Builder::new(writer);
let progress_writer = ProgressWriter::new(writer, context.clone(), file_size);
let mut builder = tokio_tar::Builder::new(progress_writer);
builder
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
@@ -608,8 +620,9 @@ where
builder.append_file(path_in_archive, &mut file).await?;
}
builder.finish().await?;
Ok(())
// Convert tar builder back into the underlying stream.
let progress_writer = builder.into_inner().await?;
Ok(progress_writer.into_inner())
}
/// Imports secret key from a file.
@@ -751,7 +764,7 @@ where
true => "private",
};
let id = id.map_or("default".into(), |i| i.to_string());
let fp = key.dc_fingerprint().hex();
let fp = DcKey::fingerprint(key).hex();
format!("{kind}-key-{addr}-{id}-{fp}.asc")
};
let path = dir.join(&file_name);
@@ -879,7 +892,7 @@ mod tests {
.unwrap()
.strip_suffix(".asc")
.unwrap();
assert_eq!(fingerprint, key.dc_fingerprint().hex());
assert_eq!(fingerprint, DcKey::fingerprint(&key).hex());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{blobdir}/{filename}");
let bytes = tokio::fs::read(&filename).await.unwrap();

View File

@@ -33,15 +33,17 @@ use std::task::Poll;
use anyhow::{bail, format_err, Context as _, Result};
use futures_lite::FutureExt;
use iroh::{Endpoint, RelayMode};
use iroh_net::relay::RelayMode;
use iroh_net::Endpoint;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::chat::add_device_msg;
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::message::Message;
use crate::imex::{BlobDirContents, ProgressReader};
use crate::message::{Message, Viewtype};
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{create_id, time, TempPathGuard};
@@ -64,11 +66,11 @@ const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
/// task use the [`Context::stop_ongoing`] mechanism.
#[derive(Debug)]
pub struct BackupProvider {
/// iroh endpoint.
/// iroh-net endpoint.
_endpoint: Endpoint,
/// iroh address.
node_addr: iroh::NodeAddr,
/// iroh-net address.
node_addr: iroh_net::NodeAddr,
/// Authentication token that should be submitted
/// to retrieve the backup.
@@ -161,7 +163,7 @@ impl BackupProvider {
async fn handle_connection(
context: Context,
conn: iroh::endpoint::Connecting,
conn: iroh_net::endpoint::Connecting,
auth_token: String,
dbfile: Arc<TempPathGuard>,
) -> Result<()> {
@@ -189,9 +191,11 @@ impl BackupProvider {
send_stream.write_all(&file_size.to_be_bytes()).await?;
export_backup_stream(&context, &dbfile, blobdir, send_stream, file_size)
.await
.context("Failed to write backup into QUIC stream")?;
let mut send_stream =
export_backup_stream(&context, &dbfile, blobdir, send_stream, file_size)
.await
.context("Failed to write backup into QUIC stream")?;
send_stream.shutdown().await?;
info!(context, "Finished writing backup into QUIC stream.");
let mut buf = [0u8; 1];
info!(context, "Waiting for acknowledgment.");
@@ -199,7 +203,8 @@ impl BackupProvider {
info!(context, "Received backup reception acknowledgement.");
context.emit_event(EventType::ImexProgress(1000));
let mut msg = Message::new_text(backup_transfer_msg_body(&context).await);
let mut msg = Message::new(Viewtype::Text);
msg.text = backup_transfer_msg_body(&context).await;
add_device_msg(&context, None, Some(&mut msg)).await?;
Ok(())
@@ -290,7 +295,7 @@ impl Future for BackupProvider {
pub async fn get_backup2(
context: &Context,
node_addr: iroh::NodeAddr,
node_addr: iroh_net::NodeAddr,
auth_token: String,
) -> Result<()> {
let relay_mode = RelayMode::Disabled;
@@ -308,7 +313,8 @@ pub async fn get_backup2(
let mut file_size_buf = [0u8; 8];
recv_stream.read_exact(&mut file_size_buf).await?;
let file_size = u64::from_be_bytes(file_size_buf);
import_backup_stream(context, recv_stream, file_size, passphrase)
let recv_stream = ProgressReader::new(recv_stream, context.clone(), file_size);
import_backup_stream(context, recv_stream, passphrase)
.await
.context("Failed to import backup from QUIC stream")?;
info!(context, "Finished importing backup from the stream.");
@@ -336,7 +342,7 @@ pub async fn get_backup2(
/// This is a long running operation which will return only when completed.
///
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
/// does avoid having [`iroh::NodeAddr`] in the primary API however, without
/// does avoid having [`iroh_net::NodeAddr`] in the primary API however, without
/// having to revert to untyped bytes.
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
match qr {
@@ -367,7 +373,6 @@ mod tests {
use std::time::Duration;
use crate::chat::{get_chat_msgs, send_msg, ChatItem};
use crate::message::Viewtype;
use crate::test_utils::TestContextManager;
use super::*;
@@ -381,7 +386,8 @@ mod tests {
// Write a message in the self chat
let self_chat = ctx0.get_self_chat().await;
let mut msg = Message::new_text("hi there".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi there".to_string());
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
// Send an attachment in the self chat

View File

@@ -11,8 +11,7 @@ use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{PublicKeyTrait, SecretKeyTrait};
use rand::thread_rng;
use pgp::types::{KeyTrait, SecretKeyTrait};
use tokio::runtime::Handle;
use crate::config::Config;
@@ -27,7 +26,7 @@ use crate::tools::{self, time_elapsed};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
@@ -94,8 +93,8 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
/// The fingerprint for the key.
fn dc_fingerprint(&self) -> Fingerprint {
PublicKeyTrait::fingerprint(self).into()
fn fingerprint(&self) -> Fingerprint {
Fingerprint::new(KeyTrait::fingerprint(self))
}
fn is_private() -> bool;
@@ -234,8 +233,7 @@ impl DcSecretKey for SignedSecretKey {
fn split_public_key(&self) -> Result<SignedPublicKey> {
self.verify()?;
let unsigned_pubkey = SecretKeyTrait::public_key(self);
let mut rng = thread_rng();
let signed_pubkey = unsigned_pubkey.sign(&mut rng, self, || "".into())?;
let signed_pubkey = unsigned_pubkey.sign(self, || "".into())?;
Ok(signed_pubkey)
}
}
@@ -397,12 +395,6 @@ impl Fingerprint {
}
}
impl From<pgp::types::Fingerprint> for Fingerprint {
fn from(fingerprint: pgp::types::Fingerprint) -> Fingerprint {
Self::new(fingerprint.as_bytes().into())
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")

View File

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

View File

@@ -244,14 +244,18 @@ impl Kml {
self.tag = KmlTag::PlacemarkPoint;
} else if tag == "coordinates" && self.tag == KmlTag::PlacemarkPoint {
self.tag = KmlTag::PlacemarkPointCoordinates;
if let Some(acc) = event.attributes().find_map(|attr| {
attr.ok().filter(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.eq_ignore_ascii_case("accuracy")
})
if let Some(acc) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| {
String::from_utf8_lossy(a.key.as_ref())
.trim()
.to_lowercase()
== "accuracy"
})
.unwrap_or_default()
}) {
let v = acc
.unwrap()
.decode_and_unescape_value(reader.decoder())
.unwrap_or_default();
@@ -286,7 +290,8 @@ pub async fn send_locations_to_chat(
)
.await?;
if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new_text(stock_str::msg_location_enabled(context).await);
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::msg_location_enabled(context).await;
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
chat::send_msg(context, chat_id, &mut msg)
.await
@@ -876,6 +881,8 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::message::MessageState;

View File

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

View File

@@ -150,17 +150,16 @@ impl MsgId {
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
update_msg_state(context, self, MessageState::OutDelivered).await?;
let chat_id: Option<ChatId> = context
let chat_id: ChatId = context
.sql
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,))
.await?;
.await?
.unwrap_or_default();
context.emit_event(EventType::MsgDelivered {
chat_id: chat_id.unwrap_or_default(),
chat_id,
msg_id: self,
});
if let Some(chat_id) = chat_id {
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}
@@ -492,15 +491,6 @@ impl Message {
}
}
/// Creates a new message with Viewtype::Text.
pub fn new_text(text: String) -> Self {
Message {
viewtype: Viewtype::Text,
text,
..Default::default()
}
}
/// Loads message with given ID from the database.
///
/// Returns an error if the message does not exist.
@@ -724,7 +714,7 @@ impl Message {
/// `contact_id` set to [`ContactId::SELF`].
///
/// `latitude` is the North-south position of the location.
/// `longitude` is the East-west position of the location.
/// `longitutde` is the East-west position of the location.
///
/// [`location::set()`]: crate::location::set
/// [`send_locations_to_chat()`]: crate::location::send_locations_to_chat
@@ -1693,7 +1683,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
m.id AS id,
m.chat_id AS chat_id,
m.state AS state,
m.download_state as download_state,
m.ephemeral_timer AS ephemeral_timer,
m.param AS param,
m.from_id AS from_id,
@@ -1709,7 +1698,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let state: MessageState = row.get("state")?;
let download_state: DownloadState = row.get("download_state")?;
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
@@ -1721,7 +1709,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
id,
chat_id,
state,
download_state,
param,
from_id,
rfc724_mid,
@@ -1751,7 +1738,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
id,
curr_chat_id,
curr_state,
curr_download_state,
curr_param,
curr_from_id,
curr_rfc724_mid,
@@ -1761,14 +1747,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
_curr_ephemeral_timer,
) in msgs
{
if curr_download_state != DownloadState::Done {
if curr_state == MessageState::InFresh {
// Don't mark partially downloaded messages as seen or send a read receipt since
// they are not really seen by the user.
update_msg_state(context, id, MessageState::InNoticed).await?;
updated_chat_ids.insert(curr_chat_id);
}
} else if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
update_msg_state(context, id, MessageState::InSeen).await?;
info!(context, "Seen message {}.", id);
@@ -1862,21 +1841,20 @@ pub(crate) async fn set_msg_failed(
}
msg.error = Some(error.to_string());
let exists = context
context
.sql
.execute(
"UPDATE msgs SET state=?, error=? WHERE id=?;",
(msg.state, error, msg.id),
)
.await?
> 0;
.await?;
context.emit_event(EventType::MsgFailed {
chat_id: msg.chat_id,
msg_id: msg.id,
});
if exists {
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
}
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
Ok(())
}
@@ -2355,7 +2333,8 @@ mod tests {
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new_text("Quoted message".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Quoted message".to_string());
// Prepare message for sending, so it gets a Message-Id.
assert!(msg.rfc724_mid.is_empty());
@@ -2421,8 +2400,9 @@ mod tests {
add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?;
// Alice quotes encrypted message in unencrypted chat.
let mut msg = Message::new_text("unencrypted".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_quote(alice, Some(&alice_received_message)).await?;
msg.set_text("unencrypted".to_string());
chat::send_msg(alice, alice_group, &mut msg).await?;
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
@@ -2480,7 +2460,8 @@ mod tests {
.unwrap();
let contact = Contact::get_by_id(&alice, contact_id).await.unwrap();
let mut msg = Message::new_text("bla blubb".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("bla blubb".to_string());
msg.set_override_sender_name(Some("over ride".to_string()));
assert_eq!(
msg.get_override_sender_name(),
@@ -2527,7 +2508,8 @@ mod tests {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let mut msg = Message::new_text("this is the text!".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("this is the text!".to_string());
// alice sends to bob,
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
@@ -2594,110 +2576,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_markseen_not_downloaded_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
.await?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert_eq!(msg.state, MessageState::InFresh);
markseen_msgs(alice, vec![msg.id]).await?;
// A not downloaded message can be seen only if it's seen on another device.
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
// Marking the message as seen again is a no op.
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::InProgress)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::Failure)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::Undecipherable)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
assert!(
!alice
.sql
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
.await?
);
alice.set_config(Config::DownloadLimit, None).await?;
// Let's assume that Alice and Bob resolved the problem with encryption.
let old_msg = msg;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, old_msg.chat_id);
assert_eq!(msg.download_state, DownloadState::Done);
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert!(msg.get_showpadlock());
// The message state mustn't be downgraded to `InFresh`.
assert_eq!(msg.state, MessageState::InNoticed);
markseen_msgs(alice, vec![msg.id]).await?;
let msg = Message::load_from_db(alice, msg.id).await?;
assert_eq!(msg.state, MessageState::InSeen);
assert_eq!(
alice
.sql
.count("SELECT COUNT(*) FROM smtp_mdns", ())
.await?,
1
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
.await?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(msg.state, MessageState::InFresh);
alice.set_config(Config::DownloadLimit, None).await?;
let seen = true;
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
.await
.unwrap()
.unwrap();
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
.await
.unwrap();
assert_eq!(msg.download_state, DownloadState::Done);
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert!(msg.get_showpadlock());
assert_eq!(msg.state, MessageState::InSeen);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_state() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -2716,7 +2594,8 @@ mod tests {
}
// check outgoing messages states on sender side
let mut alice_msg = Message::new_text("hi!".to_string());
let mut alice_msg = Message::new(Viewtype::Text);
alice_msg.set_text("hi!".to_string());
assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work
alice_chat
@@ -2899,7 +2778,8 @@ def hello():
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
let mut msg = Message::new_text("hi".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi".to_string());
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
.await
.is_err());

View File

@@ -6,6 +6,7 @@ use anyhow::{bail, Context as _, Result};
use base64::Engine as _;
use chrono::TimeZone;
use email::Mailbox;
use format_flowed::{format_flowed, format_flowed_quote};
use lettre_email::{Address, Header, MimeMultipartType, PartBuilder};
use tokio::fs;
@@ -356,7 +357,7 @@ impl MimeFactory {
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
// Otherwise "smeared timestamps" may result in the condition
// Othewise "smeared timestamps" may result in the condition
// to fail even if the clock is monotonic.
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
Ok(true)
@@ -742,9 +743,7 @@ impl MimeFactory {
hidden_headers.push(header);
} else if header_name == "chat-user-avatar" {
hidden_headers.push(header);
} else if header_name == "autocrypt"
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
{
} else if header_name == "autocrypt" {
unprotected_headers.push(header.clone());
} else if header_name == "from" {
// Unencrypted securejoin messages should _not_ include the display name:
@@ -1299,18 +1298,9 @@ impl MimeFactory {
let final_text = placeholdertext.as_deref().unwrap_or(&msg.text);
let mut quoted_text = None;
if let Some(msg_quoted_text) = msg.quoted_text() {
let mut some_quoted_text = String::new();
for quoted_line in msg_quoted_text.split('\n') {
some_quoted_text += "> ";
some_quoted_text += quoted_line;
some_quoted_text += "\r\n";
}
some_quoted_text += "\r\n";
quoted_text = Some(some_quoted_text)
}
let mut quoted_text = msg
.quoted_text()
.map(|quote| format_flowed_quote(&quote) + "\r\n\r\n");
if !is_encrypted && msg.param.get_bool(Param::ProtectQuote).unwrap_or_default() {
// Message is not encrypted but quotes encrypted message.
quoted_text = Some("> ...\r\n\r\n".to_string());
@@ -1320,6 +1310,7 @@ impl MimeFactory {
// Delta Chat.
quoted_text = Some("\r\n".to_string());
}
let flowed_text = format_flowed(final_text);
let is_reaction = msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
@@ -1329,7 +1320,7 @@ impl MimeFactory {
"{}{}{}{}{}{}",
fwdhint.unwrap_or_default(),
quoted_text.unwrap_or_default(),
escape_message_footer_marks(final_text),
escape_message_footer_marks(&flowed_text),
if !final_text.is_empty() && !footer.is_empty() {
"\r\n\r\n"
} else {
@@ -1339,8 +1330,10 @@ impl MimeFactory {
footer
);
let mut main_part =
PartBuilder::new().header(("Content-Type", "text/plain; charset=utf-8"));
let mut main_part = PartBuilder::new().header((
"Content-Type",
"text/plain; charset=utf-8; format=flowed; delsp=no",
));
main_part = self.add_message_text(main_part, message_text);
if is_reaction {
@@ -1989,7 +1982,8 @@ mod tests {
group_id: ChatId,
quote: Option<&Message>,
) -> Result<String> {
let mut new_msg = Message::new_text("Hi".to_string());
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text("Hi".to_string());
if let Some(q) = quote {
new_msg.set_quote(t, Some(q)).await?;
}
@@ -2075,7 +2069,8 @@ mod tests {
let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap();
let mut new_msg = Message::new_text("Hi".to_string());
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
@@ -2182,7 +2177,8 @@ mod tests {
let chat_id = chats.get_chat_id(0).unwrap();
chat_id.accept(context).await.unwrap();
let mut new_msg = Message::new_text("Hi".to_string());
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text("Hi".to_string());
new_msg.chat_id = chat_id;
chat::prepare_msg(context, chat_id, &mut new_msg)
.await
@@ -2279,6 +2275,7 @@ mod tests {
let msg = message.as_string();
let header_end = msg.find("Hi").unwrap();
#[allow(clippy::indexing_slicing)]
let headers = msg[0..header_end].trim();
assert!(!headers.lines().any(|l| l.trim().is_empty()));
@@ -2298,7 +2295,8 @@ mod tests {
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new_text("this is the text!".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
@@ -2364,7 +2362,8 @@ mod tests {
// send message to bob: that should get multipart/signed.
// `Subject:` is protected by copying it.
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new_text("this is the text!".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
@@ -2497,7 +2496,8 @@ mod tests {
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new_text("this is the text!".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let payload = sent_msg.payload();

View File

@@ -4,7 +4,6 @@ use std::cmp::min;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::str;
use std::str::FromStr;
use anyhow::{bail, Context as _, Result};
use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
@@ -15,7 +14,6 @@ use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, Si
use rand::distributions::{Alphanumeric, DistString};
use crate::aheader::{Aheader, EncryptPreference};
use crate::authres::handle_authres;
use crate::blob::BlobObject;
use crate::chat::{add_info_msg, ChatId};
use crate::config::Config;
@@ -23,8 +21,8 @@ use crate::constants::{self, Chattype};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::decrypt::{
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
validate_detached_signature,
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature,
DecryptionInfo,
};
use crate::dehtml::dehtml;
use crate::events::EventType;
@@ -73,8 +71,7 @@ pub(crate) struct MimeMessage {
/// messages to this address to post them to the list.
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub autocrypt_header: Option<Aheader>,
pub peerstate: Option<Peerstate>,
pub decryption_info: DecryptionInfo,
pub decrypting_failed: bool,
/// Set of valid signature fingerprints if a message is an
@@ -304,101 +301,42 @@ impl MimeMessage {
let mut from = from.context("No from in message")?;
let private_keyring = load_self_secret_keyring(context).await?;
let allow_aeap = get_encrypted_mime(&mail).is_some();
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, timestamp_sent).await?;
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossiped_keys = Default::default();
let mut from_is_signed = false;
hop_info += "\n\n";
hop_info += &dkim_results.to_string();
hop_info += &decryption_info.dkim_results.to_string();
let incoming = !context.is_self_addr(&from.addr).await?;
let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
let mail_raw; // Memory location for a possible decrypted message.
let decrypted_msg; // Decrypted signed OpenPGP message.
let (mail, encrypted) =
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
Ok(Some(msg)) => {
mail_raw = msg.get_content()?.unwrap_or_default();
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
decrypted_msg = Some(msg);
if let Some(protected_aheader_value) = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
{
aheader_value = Some(protected_aheader_value);
}
(Ok(decrypted_mail), true)
}
Ok(None) => {
mail_raw = Vec::new();
decrypted_msg = None;
(Ok(mail), false)
}
Err(err) => {
mail_raw = Vec::new();
decrypted_msg = None;
warn!(context, "decryption failed: {:#}", err);
(Err(err), false)
}
};
let autocrypt_header = if !incoming {
None
} else if let Some(aheader_value) = aheader_value {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
} else {
None
};
// The peerstate that will be used to validate the signatures.
let mut peerstate = get_autocrypt_peerstate(
context,
&from.addr,
autocrypt_header.as_ref(),
timestamp_sent,
allow_aeap,
)
.await?;
let public_keyring = match peerstate.is_none() && !incoming {
let public_keyring = match decryption_info.peerstate.is_none() && !incoming {
true => key::load_self_public_keyring(context).await?,
false => keyring_from_peerstate(peerstate.as_ref()),
false => keyring_from_peerstate(decryption_info.peerstate.as_ref()),
};
let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)?
} else {
HashSet::new()
let (mail, mut signatures, encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &public_keyring)
}) {
Ok(Some((raw, signatures))) => {
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
(Ok(decrypted_mail), signatures, true)
}
Ok(None) => (Ok(mail), HashSet::new(), false),
Err(err) => {
warn!(context, "decryption failed: {:#}", err);
(Err(err), HashSet::new(), false)
}
};
let mail = mail.as_ref().map(|mail| {
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
.unwrap_or((mail, Default::default()));
@@ -484,7 +422,7 @@ impl MimeMessage {
Self::remove_secured_headers(&mut headers);
// If it is not a read receipt, degrade encryption.
if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, mail) {
if timestamp_sent > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report"
{
@@ -495,7 +433,7 @@ impl MimeMessage {
if !encrypted {
signatures.clear();
}
if let Some(peerstate) = &mut peerstate {
if let Some(peerstate) = &mut decryption_info.peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await?;
@@ -511,8 +449,7 @@ impl MimeMessage {
from_is_signed,
incoming,
chat_disposition_notification_to,
autocrypt_header,
peerstate,
decryption_info,
decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message
@@ -665,13 +602,11 @@ impl MimeMessage {
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
/// containing a description. If such a message is detected, text from the first part can be
/// moved to the second part, and the first part dropped.
#[allow(clippy::indexing_slicing)]
fn squash_attachment_parts(&mut self) {
if self.parts.len() == 2
&& self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
&& self
.parts
.get(1)
.is_some_and(|filepart| match filepart.typ {
if let [textpart, filepart] = &self.parts[..] {
let need_drop = textpart.typ == Viewtype::Text
&& match filepart.typ {
Viewtype::Image
| Viewtype::Gif
| Viewtype::Sticker
@@ -682,24 +617,24 @@ impl MimeMessage {
| Viewtype::File
| Viewtype::Webxdc => true,
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
})
{
let mut parts = std::mem::take(&mut self.parts);
let Some(mut filepart) = parts.pop() else {
// Should never happen.
return;
};
let Some(textpart) = parts.pop() else {
// Should never happen.
return;
};
};
filepart.msg.clone_from(&textpart.msg);
if let Some(quote) = textpart.param.get(Param::Quote) {
filepart.param.set(Param::Quote, quote);
if need_drop {
let mut filepart = self.parts.swap_remove(1);
// insert new one
filepart.msg.clone_from(&self.parts[0].msg);
if let Some(quote) = self.parts[0].param.get(Param::Quote) {
filepart.param.set(Param::Quote, quote);
}
// forget the one we use now
self.parts[0].msg = "".to_string();
// swap new with old
self.parts.push(filepart); // push to the end
let _ = self.parts.swap_remove(0); // drops first element, replacing it with the last one in O(1)
}
self.parts = vec![filepart];
}
}
@@ -1223,7 +1158,7 @@ impl MimeMessage {
let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
{
format.as_str().eq_ignore_ascii_case("flowed")
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
};
@@ -1233,7 +1168,7 @@ impl MimeMessage {
&& is_format_flowed
{
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().eq_ignore_ascii_case("yes")
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
};
@@ -1296,7 +1231,7 @@ impl MimeMessage {
if decoded_data.is_empty() {
return Ok(());
}
if let Some(peerstate) = &mut self.peerstate {
if let Some(peerstate) = &mut self.decryption_info.peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual
&& mime_type.type_() == mime::APPLICATION
&& mime_type.subtype().as_str() == "pgp-keys"
@@ -1667,11 +1602,10 @@ impl MimeMessage {
{
let mut to_list =
get_all_addresses_from_header(&report.headers, "x-failed-recipients");
let to = if to_list.len() != 1 {
// We do not know which recipient failed
None
let to = if to_list.len() == 1 {
Some(to_list.pop().unwrap())
} else {
to_list.pop()
None // We do not know which recipient failed
};
return Ok(Some(DeliveryReport {
@@ -1754,6 +1688,7 @@ impl MimeMessage {
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
/// Also you should add a test in receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) {
let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
@@ -1916,17 +1851,18 @@ pub(crate) struct DeliveryReport {
pub failure: bool,
}
#[allow(clippy::indexing_slicing)]
pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
let mut msgids = Vec::new();
for id in ids.split_whitespace() {
let mut id = id.to_string();
if let Some(id_without_prefix) = id.strip_prefix('<') {
id = id_without_prefix.to_string();
};
if let Some(id_without_suffix) = id.strip_suffix('>') {
id = id_without_suffix.to_string();
};
if id.starts_with('<') {
id = id[1..].to_string();
}
if id.ends_with('>') {
id = id[..id.len() - 1].to_string();
}
if !id.is_empty() {
msgids.push(id);
}
@@ -2304,22 +2240,12 @@ async fn handle_ndn(
} else {
"Delivery to at least one recipient failed.".to_string()
};
let err_msg = &error;
let mut first = true;
for msg in msgs {
let (msg_id, chat_id, chat_type) = msg?;
let mut message = Message::load_from_db(context, msg_id).await?;
let aggregated_error = message
.error
.as_ref()
.map(|err| format!("{}\n\n{}", err, err_msg));
set_msg_failed(
context,
&mut message,
aggregated_error.as_ref().unwrap_or(err_msg),
)
.await?;
set_msg_failed(context, &mut message, &error).await?;
if first {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
@@ -2363,6 +2289,8 @@ async fn ndn_maybe_add_info_msg(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use mailparse::ParsedMail;
use super::*;
@@ -3670,24 +3598,11 @@ On 2020-10-25, Bob wrote:
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
}
for draft in [false, true] {
{
let chat = t.get_self_chat().await;
let mut msg = Message::new_text(long_txt.clone());
if draft {
chat.id.set_draft(&t, Some(&mut msg)).await?;
}
t.send_msg(chat.id, &mut msg).await;
t.send_text(chat.id, &long_txt).await;
let msg = t.get_last_msg_in(chat.id).await;
assert!(msg.has_html());
assert_eq!(
msg.id
.get_html(&t)
.await?
.unwrap()
.matches("just repeated")
.count(),
REPEAT_CNT
);
assert!(
msg.text.matches("just repeated").count() <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
@@ -3695,6 +3610,7 @@ On 2020-10-25, Bob wrote:
}
t.set_config(Config::Bot, Some("1")).await?;
{
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?;
assert!(!mimemsg.is_mime_modified);
@@ -3707,28 +3623,6 @@ On 2020-10-25, Bob wrote:
Ok(())
}
/// Tests that sender status (signature) does not appear
/// in HTML view of a long message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_large_message_no_signature() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config(Config::Selfstatus, Some("Some signature"))
.await?;
let chat = alice.create_chat(bob).await;
let txt = "Hello!\n".repeat(500);
let sent = alice.send_text(chat.id, &txt).await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.has_html(), true);
let html = msg.id.get_html(bob).await?.unwrap();
assert_eq!(html.contains("Some signature"), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_x_microsoft_original_message_id() {
let t = TestContext::new_alice().await;
@@ -4096,8 +3990,12 @@ Content-Disposition: reaction\n\
// We do allow the time to be in the future a bit (because of unsynchronized clocks),
// but only 60 seconds:
assert!(mime_message.timestamp_sent <= time() + 60);
assert!(mime_message.timestamp_sent >= beginning_time + 60);
assert!(mime_message.decryption_info.message_time <= time() + 60);
assert!(mime_message.decryption_info.message_time >= beginning_time + 60);
assert_eq!(
mime_message.decryption_info.message_time,
mime_message.timestamp_sent
);
assert!(mime_message.timestamp_rcvd <= time());
Ok(())
@@ -4168,24 +4066,4 @@ Content-Type: text/plain; charset=utf-8
"alice@example.org"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protect_autocrypt() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config_bool(Config::ProtectAutocrypt, true)
.await?;
bob.set_config_bool(Config::ProtectAutocrypt, true).await?;
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
assert_eq!(msg.get_showpadlock(), false);
let msg = tcm.send_recv(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
Ok(())
}
}

View File

@@ -5,13 +5,13 @@ use std::pin::Pin;
use std::time::Duration;
use anyhow::{format_err, Context as _, Result};
use async_native_tls::TlsStream;
use tokio::net::TcpStream;
use tokio::task::JoinSet;
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::net::session::SessionStream;
use crate::sql::Sql;
use crate::tools::time;
@@ -47,7 +47,7 @@ pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
Ok(())
}
/// Update the timestamp of the last successful connection
/// Update the timestamp of the last successfull connection
/// to the given `host` and `port`
/// with the given application protocol `alpn`.
///
@@ -128,7 +128,7 @@ pub(crate) async fn connect_tls_inner(
host: &str,
strict_tls: bool,
alpn: &[&str],
) -> Result<impl SessionStream> {
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
Ok(tls_stream)

View File

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

View File

@@ -2,39 +2,45 @@
use std::sync::Arc;
use anyhow::Result;
use async_native_tls::{Certificate, Protocol, TlsConnector, TlsStream};
use once_cell::sync::Lazy;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::net::session::SessionStream;
// this certificate is missing on older android devices (eg. lg with android6 from 2017)
// certificate downloaded from https://letsencrypt.org/certificates/
static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
pub async fn wrap_tls(
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
strict_tls: bool,
hostname: &str,
alpn: &[&str],
stream: impl SessionStream + 'static,
) -> Result<impl SessionStream> {
if strict_tls {
let tls_stream = wrap_rustls(hostname, alpn, stream).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
stream: T,
) -> Result<TlsStream<T>> {
let tls_builder = TlsConnector::new()
.min_protocol_version(Some(Protocol::Tlsv12))
.request_alpns(alpn)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
let tls = if strict_tls {
tls_builder
} else {
// We use native_tls because it accepts 1024-bit RSA keys.
// Rustls does not support them even if
// certificate checks are disabled: <https://github.com/rustls/rustls/issues/234>.
let tls = async_native_tls::TlsConnector::new()
.min_protocol_version(Some(async_native_tls::Protocol::Tlsv12))
.request_alpns(alpn)
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true);
let tls_stream = tls.connect(hostname, stream).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
}
.danger_accept_invalid_certs(true)
};
let tls_stream = tls.connect(hostname, stream).await?;
Ok(tls_stream)
}
pub async fn wrap_rustls(
pub async fn wrap_rustls<T: AsyncRead + AsyncWrite + Unpin>(
hostname: &str,
alpn: &[&str],
stream: impl SessionStream,
) -> Result<impl SessionStream> {
stream: T,
) -> Result<tokio_rustls::client::TlsStream<T>> {
let mut root_cert_store = rustls::RootCertStore::empty();
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());

View File

@@ -7,15 +7,15 @@
//! when it's not required. Only when a webxdc subscribes to realtime data or when a reatlime message is sent,
//! the p2p machinery should be started.
//!
//! Adding peer channels to webxdc needs upfront negotiation of a topic and sharing of public keys so that
//! Adding peer channels to webxdc needs upfront negotation of a topic and sharing of public keys so that
//! nodes can connect to each other. The explicit approach is as follows:
//!
//! 1. We introduce a new [`IrohGossipTopic`](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
//! 1. We introduce a new [GossipTopic](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
//! securely generated on the initial webxdc sender's device. This message header is encrypted
//! and sent in the same message as the webxdc application.
//! 2. Whenever `joinRealtimeChannel().setListener()` or `joinRealtimeChannel().send()` is called by the webxdc application,
//! we start a routine to establish p2p connectivity and join the gossip swarm with Iroh.
//! 3. The first step of this routine is to introduce yourself with a regular message containing the [`IrohNodeAddr`](crate::headerdef::HeaderDef::IrohNodeAddr).
//! 3. The first step of this routine is to introduce yourself with a regular message containing the `IrohPublicKey`.
//! This message contains the users relay-server and public key.
//! Direct IP address is not included as this information can be persisted by email providers.
//! 4. After the announcement, the sending peer joins the gossip swarm with an empty list of peer IDs (as they don't know anyone yet).
@@ -26,14 +26,15 @@
use anyhow::{anyhow, bail, Context as _, Result};
use email::Header;
use futures_lite::StreamExt;
use iroh::key::{PublicKey, SecretKey};
use iroh::{Endpoint, NodeAddr, NodeId, RelayMap, RelayMode, RelayUrl};
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
use iroh_gossip::proto::TopicId;
use iroh_net::key::{PublicKey, SecretKey};
use iroh_net::relay::{RelayMap, RelayUrl};
use iroh_net::{relay::RelayMode, Endpoint};
use iroh_net::{NodeAddr, NodeId};
use parking_lot::Mutex;
use std::collections::{BTreeSet, HashMap};
use std::env;
use std::sync::Arc;
use tokio::sync::{oneshot, RwLock};
use tokio::task::JoinHandle;
use url::Url;
@@ -53,11 +54,11 @@ const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
/// Store iroh peer channels for the context.
#[derive(Debug)]
pub struct Iroh {
/// iroh router needed for iroh peer channels.
pub(crate) router: iroh::protocol::Router,
/// [Endpoint] needed for iroh peer channels.
pub(crate) endpoint: Endpoint,
/// [Gossip] needed for iroh peer channels.
pub(crate) gossip: Arc<Gossip>,
pub(crate) gossip: Gossip,
/// Sequence numbers for gossip channels.
pub(crate) sequence_numbers: Mutex<HashMap<TopicId, i32>>,
@@ -74,12 +75,7 @@ pub struct Iroh {
impl Iroh {
/// Notify the endpoint that the network has changed.
pub(crate) async fn network_change(&self) {
self.router.endpoint().network_change().await
}
/// Closes the QUIC endpoint.
pub(crate) async fn close(self) -> Result<()> {
self.router.shutdown().await.context("Closing iroh failed")
self.endpoint.network_change().await
}
/// Join a topic and create the subscriber loop for it.
@@ -117,7 +113,7 @@ impl Iroh {
// Inform iroh of potentially new node addresses
for node_addr in &peers {
if !node_addr.info.is_empty() {
self.router.endpoint().add_node_addr(node_addr.clone())?;
self.endpoint.add_node_addr(node_addr.clone())?;
}
}
@@ -144,7 +140,7 @@ impl Iroh {
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
if self.iroh_channels.read().await.get(&topic).is_some() {
for peer in &peers {
self.router.endpoint().add_node_addr(peer.clone())?;
self.endpoint.add_node_addr(peer.clone())?;
}
self.gossip.join_with_opts(
@@ -194,7 +190,7 @@ impl Iroh {
/// Get the iroh [NodeAddr] without direct IP addresses.
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
let mut addr = self.router.endpoint().node_addr().await?;
let mut addr = self.endpoint.node_addr().await?;
addr.info.direct_addresses = BTreeSet::new();
Ok(addr)
}
@@ -271,19 +267,16 @@ impl Context {
max_message_size: 128 * 1024,
..Default::default()
};
let gossip = Arc::new(Gossip::from_endpoint(
endpoint.clone(),
gossip_config,
&my_addr.info,
));
let gossip = Gossip::from_endpoint(endpoint.clone(), gossip_config, &my_addr.info);
let router = iroh::protocol::Router::builder(endpoint)
.accept(GOSSIP_ALPN, gossip.clone())
.spawn()
.await?;
// spawn endpoint loop that forwards incoming connections to the gossiper
let context = self.clone();
// Shuts down on deltachat shutdown
tokio::spawn(endpoint_loop(context, endpoint.clone(), gossip.clone()));
Ok(Iroh {
router,
endpoint,
gossip,
sequence_numbers: Mutex::new(HashMap::new()),
iroh_channels: RwLock::new(HashMap::new()),
@@ -292,36 +285,15 @@ impl Context {
}
/// Get or initialize the iroh peer channel.
pub async fn get_or_try_init_peer_channel(
&self,
) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
pub async fn get_or_try_init_peer_channel(&self) -> Result<&Iroh> {
if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
bail!("Attempt to get Iroh when realtime is disabled");
}
if let Ok(lock) = tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
self.iroh.read().await,
|opt_iroh| opt_iroh.as_ref(),
) {
return Ok(lock);
}
let lock = self.iroh.write().await;
match tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
lock,
|opt_iroh| opt_iroh.as_ref(),
) {
Ok(lock) => Ok(lock),
Err(mut lock) => {
let iroh = self.init_peer_channels().await?;
*lock = Some(iroh);
tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
lock,
|opt_iroh| opt_iroh.as_ref(),
)
.map_err(|_| anyhow!("Downgrade should succeed as we just stored `Some` value"))
}
}
let ctx = self.clone();
self.iroh
.get_or_try_init(|| async { ctx.init_peer_channels().await })
.await
}
}
@@ -342,47 +314,6 @@ pub(crate) async fn iroh_add_peer_for_topic(
Ok(())
}
/// Add gossip peer from `Iroh-Node-Addr` header to WebXDC message identified by `instance_id`.
pub async fn add_gossip_peer_from_header(
context: &Context,
instance_id: MsgId,
node_addr: &str,
) -> Result<()> {
if !context
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await?
{
return Ok(());
}
info!(
context,
"Adding iroh peer with address {node_addr:?} to the topic of {instance_id}."
);
let node_addr =
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
msg_id: instance_id,
});
let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? else {
warn!(
context,
"Could not add iroh peer because {instance_id} has no topic."
);
return Ok(());
};
let node_id = node_addr.node_id;
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
let iroh = context.get_or_try_init_peer_channel().await?;
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
Ok(())
}
/// Insert topicId into the database so that we can use it to retrieve the topic.
pub(crate) async fn insert_topic_stub(ctx: &Context, msg_id: MsgId, topic: TopicId) -> Result<()> {
ctx.sql
@@ -506,13 +437,54 @@ fn create_random_topic() -> TopicId {
pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result<Header> {
let topic = create_random_topic();
insert_topic_stub(ctx, msg_id, topic).await?;
let topic_string = iroh_base::base32::fmt(topic.as_bytes());
Ok(Header::new(
HeaderDef::IrohGossipTopic.get_headername().to_string(),
topic_string,
topic.to_string(),
))
}
async fn endpoint_loop(context: Context, endpoint: Endpoint, gossip: Gossip) {
while let Some(conn) = endpoint.accept().await {
let conn = match conn.accept() {
Ok(conn) => conn,
Err(err) => {
warn!(context, "Failed to accept iroh connection: {err:#}.");
continue;
}
};
info!(context, "IROH_REALTIME: accepting iroh connection");
let gossip = gossip.clone();
let context = context.clone();
tokio::spawn(async move {
if let Err(err) = handle_connection(&context, conn, gossip).await {
warn!(context, "IROH_REALTIME: iroh connection error: {err}");
}
});
}
}
async fn handle_connection(
context: &Context,
mut conn: iroh_net::endpoint::Connecting,
gossip: Gossip,
) -> anyhow::Result<()> {
let alpn = conn.alpn().await?;
let conn = conn.await?;
let peer_id = iroh_net::endpoint::get_remote_node_id(&conn)?;
match alpn.as_slice() {
GOSSIP_ALPN => gossip
.handle_connection(conn)
.await
.context(format!("Gossip connection to {peer_id} failed"))?,
_ => warn!(
context,
"Ignoring connection from {peer_id}: unsupported ALPN protocol"
),
}
Ok(())
}
async fn subscribe_loop(
context: &Context,
mut stream: iroh_gossip::net::GossipReceiver,
@@ -563,8 +535,6 @@ async fn subscribe_loop(
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::{
chat::send_msg,
@@ -579,6 +549,17 @@ mod tests {
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
// Alice sends webxdc to bob
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
@@ -615,6 +596,7 @@ mod tests {
break;
}
}
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
// Bob adds alice to gossip peers.
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
@@ -624,23 +606,13 @@ mod tests {
.map(|addr| addr.node_id)
.collect::<Vec<_>>();
let alice_iroh = alice.get_or_try_init_peer_channel().await.unwrap();
assert_eq!(
members,
vec![
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
.get_node_addr()
.await
.unwrap()
.node_id
]
vec![alice_iroh.get_node_addr().await.unwrap().node_id]
);
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
@@ -649,10 +621,7 @@ mod tests {
.unwrap();
// Alice sends ephemeral message
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
alice_iroh
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
.await
.unwrap();
@@ -671,9 +640,7 @@ mod tests {
}
}
// Bob sends ephemeral message
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.await
.unwrap();
@@ -702,20 +669,10 @@ mod tests {
assert_eq!(
members,
vec![
bob.get_or_try_init_peer_channel()
.await
.unwrap()
.get_node_addr()
.await
.unwrap()
.node_id
]
vec![bob_iroh.get_node_addr().await.unwrap().node_id]
);
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
.await
.unwrap();
@@ -733,27 +690,25 @@ 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)]
async fn test_can_reconnect() {
tokio::time::timeout(Duration::from_secs(20), test_can_reconnect_impl())
.await
.unwrap();
}
async fn test_can_reconnect_impl() {
tracing_subscriber::fmt::init();
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
assert!(alice
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await
@@ -787,6 +742,7 @@ mod tests {
.unwrap();
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
// Bob adds alice to gossip peers.
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
@@ -796,23 +752,13 @@ mod tests {
.map(|addr| addr.node_id)
.collect::<Vec<_>>();
let alice_iroh = alice.get_or_try_init_peer_channel().await.unwrap();
assert_eq!(
members,
vec![
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
.get_node_addr()
.await
.unwrap()
.node_id
]
vec![alice_iroh.get_node_addr().await.unwrap().node_id]
);
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
@@ -821,10 +767,7 @@ mod tests {
.unwrap();
// Alice sends ephemeral message
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
alice_iroh
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
.await
.unwrap();
@@ -849,9 +792,7 @@ mod tests {
.unwrap();
let bob_sequence_number = bob
.iroh
.read()
.await
.as_ref()
.get()
.unwrap()
.sequence_numbers
.lock()
@@ -860,9 +801,7 @@ mod tests {
leave_webxdc_realtime(bob, bob_webxdc.id).await.unwrap();
let bob_sequence_number_after = bob
.iroh
.read()
.await
.as_ref()
.get()
.unwrap()
.sequence_numbers
.lock()
@@ -871,9 +810,7 @@ mod tests {
// Check that sequence number is persisted when leaving the channel.
assert_eq!(bob_sequence_number, bob_sequence_number_after);
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
@@ -881,9 +818,7 @@ mod tests {
.await
.unwrap();
bob.get_or_try_init_peer_channel()
.await
.unwrap()
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.await
.unwrap();
@@ -902,20 +837,11 @@ mod tests {
}
}
// channel is only used to remember if an advertisement has been sent
// channel is only used to remeber if an advertisement has been sent
// bob for example does not change the channels because he never sends an
// advertisement
assert_eq!(
alice
.iroh
.read()
.await
.as_ref()
.unwrap()
.iroh_channels
.read()
.await
.len(),
alice.iroh.get().unwrap().iroh_channels.read().await.len(),
1
);
leave_webxdc_realtime(alice, alice_webxdc.id).await.unwrap();
@@ -925,9 +851,7 @@ mod tests {
.unwrap();
assert!(alice
.iroh
.read()
.await
.as_ref()
.get()
.unwrap()
.iroh_channels
.read()
@@ -938,11 +862,21 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parallel_connect() {
eprintln!("START-----");
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
bob.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
alice
.ctx
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
// Alice sends webxdc to bob
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
@@ -1011,29 +945,24 @@ mod tests {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
alice
.set_config_bool(Config::WebxdcRealtimeEnabled, false)
.await
.unwrap();
// creates iroh endpoint as side effect
send_webxdc_realtime_advertisement(alice, MsgId::new(1))
.await
.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
assert!(alice.ctx.iroh.get().is_none());
// creates iroh endpoint as side effect
send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
.await
.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
assert!(alice.ctx.iroh.get().is_none());
// creates iroh endpoint as side effect
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
assert!(alice.ctx.iroh.get().is_none());
// This internal function should return error
// if accidentally called with the setting disabled.

View File

@@ -121,7 +121,7 @@ impl Peerstate {
last_seen_autocrypt: last_seen,
prefer_encrypt,
public_key: Some(public_key.clone()),
public_key_fingerprint: Some(public_key.dc_fingerprint()),
public_key_fingerprint: Some(public_key.fingerprint()),
gossip_key: None,
gossip_key_fingerprint: None,
gossip_timestamp: 0,
@@ -153,7 +153,7 @@ impl Peerstate {
public_key: None,
public_key_fingerprint: None,
gossip_key: Some(gossip_header.public_key.clone()),
gossip_key_fingerprint: Some(gossip_header.public_key.dc_fingerprint()),
gossip_key_fingerprint: Some(gossip_header.public_key.fingerprint()),
gossip_timestamp: message_time,
verified_key: None,
verified_key_fingerprint: None,
@@ -308,7 +308,7 @@ impl Peerstate {
pub fn recalc_fingerprint(&mut self) {
if let Some(ref public_key) = self.public_key {
let old_public_fingerprint = self.public_key_fingerprint.take();
self.public_key_fingerprint = Some(public_key.dc_fingerprint());
self.public_key_fingerprint = Some(public_key.fingerprint());
if old_public_fingerprint.is_some()
&& old_public_fingerprint != self.public_key_fingerprint
@@ -319,7 +319,7 @@ impl Peerstate {
if let Some(ref gossip_key) = self.gossip_key {
let old_gossip_fingerprint = self.gossip_key_fingerprint.take();
self.gossip_key_fingerprint = Some(gossip_key.dc_fingerprint());
self.gossip_key_fingerprint = Some(gossip_key.fingerprint());
if old_gossip_fingerprint.is_none()
|| self.gossip_key_fingerprint.is_none()
@@ -506,7 +506,7 @@ impl Peerstate {
fingerprint: Fingerprint,
verifier: String,
) -> Result<()> {
if key.dc_fingerprint() == fingerprint {
if key.fingerprint() == fingerprint {
self.verified_key = Some(key);
self.verified_key_fingerprint = Some(fingerprint);
self.verifier = Some(verifier);
@@ -524,7 +524,7 @@ impl Peerstate {
/// do nothing to avoid overwriting secondary verified key
/// which may be different.
pub fn set_secondary_verified_key(&mut self, gossip_key: SignedPublicKey, verifier: String) {
let fingerprint = gossip_key.dc_fingerprint();
let fingerprint = gossip_key.fingerprint();
if self.verified_key_fingerprint.as_ref() != Some(&fingerprint) {
self.secondary_verified_key = Some(gossip_key);
self.secondary_verified_key_fingerprint = Some(fingerprint);
@@ -542,8 +542,6 @@ impl Peerstate {
/// * `old_addr`: Old address of the peerstate in case of an AEAP transition.
pub(crate) async fn save_to_db_ex(&self, sql: &Sql, old_addr: Option<&str>) -> Result<()> {
let trans_fn = |t: &mut rusqlite::Transaction| {
let verified_key_fingerprint =
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex());
if let Some(old_addr) = old_addr {
// We are doing an AEAP transition to the new address and the SQL INSERT below will
// save the existing peerstate as belonging to this new address. We now need to
@@ -553,14 +551,11 @@ impl Peerstate {
// existing peerstate as this would break encryption to it. This is critical for
// non-verified groups -- if we can't encrypt to the old address, we can't securely
// remove it from the group (to add the new one instead).
//
// NB: We check that `verified_key_fingerprint` hasn't changed to protect from
// possible races.
t.execute(
"UPDATE acpeerstates
SET verified_key=NULL, verified_key_fingerprint='', verifier=''
WHERE addr=? AND verified_key_fingerprint=?",
(old_addr, &verified_key_fingerprint),
"UPDATE acpeerstates \
SET verified_key=NULL, verified_key_fingerprint='', verifier='' \
WHERE addr=?",
(old_addr,),
)?;
}
t.execute(
@@ -609,7 +604,7 @@ impl Peerstate {
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
&verified_key_fingerprint,
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verifier.as_deref().unwrap_or(""),
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
self.secondary_verified_key_fingerprint
@@ -771,7 +766,8 @@ pub(crate) async fn maybe_do_aeap_transition(
context: &Context,
mime_parser: &mut crate::mimeparser::MimeMessage,
) -> Result<()> {
let Some(peerstate) = &mime_parser.peerstate else {
let info = &mime_parser.decryption_info;
let Some(peerstate) = &info.peerstate else {
return Ok(());
};
@@ -819,13 +815,13 @@ pub(crate) async fn maybe_do_aeap_transition(
// DC avoids sending messages with the same timestamp, that's why messages
// with equal timestamps are ignored here unlike in `Peerstate::apply_header()`.
if mime_parser.timestamp_sent <= peerstate.last_seen {
if info.message_time <= peerstate.last_seen {
info!(
context,
"Not doing AEAP from {} to {} because {} < {}.",
&peerstate.addr,
&mime_parser.from.addr,
mime_parser.timestamp_sent,
info.message_time,
peerstate.last_seen
);
return Ok(());
@@ -836,23 +832,24 @@ pub(crate) async fn maybe_do_aeap_transition(
"Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
);
let peerstate = mime_parser.peerstate.as_mut().context("no peerstate??")?;
let info = &mut mime_parser.decryption_info;
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
// Add info messages to chats with this (verified) contact
//
peerstate
.handle_setup_change(
context,
mime_parser.timestamp_sent,
PeerstateChange::Aeap(mime_parser.from.addr.clone()),
info.message_time,
PeerstateChange::Aeap(info.from.clone()),
)
.await?;
let old_addr = mem::take(&mut peerstate.addr);
peerstate.addr.clone_from(&mime_parser.from.addr);
let header = mime_parser.autocrypt_header.as_ref().context(
peerstate.addr.clone_from(&info.from);
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(context, header, mime_parser.timestamp_sent);
peerstate.apply_header(context, header, info.message_time);
peerstate
.save_to_db_ex(&context.sql, Some(&old_addr))
@@ -893,12 +890,12 @@ mod tests {
last_seen_autocrypt: 11,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 12,
gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
@@ -918,7 +915,7 @@ mod tests {
.expect("no peerstate found in the database");
assert_eq!(peerstate, peerstate_new);
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.dc_fingerprint())
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
@@ -937,7 +934,7 @@ mod tests {
last_seen_autocrypt: 11,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: None,
gossip_timestamp: 12,
gossip_key_fingerprint: None,
@@ -974,7 +971,7 @@ mod tests {
last_seen_autocrypt: 11,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: None,
gossip_timestamp: 12,
gossip_key_fingerprint: None,

View File

@@ -14,7 +14,7 @@ use pgp::composed::{
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey};
use pgp::types::{CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, StringToKey};
use rand::{thread_rng, CryptoRng, Rng};
use tokio::runtime::Handle;
@@ -41,15 +41,8 @@ enum SignedPublicKeyOrSubkey<'a> {
Subkey(&'a SignedPublicSubKey),
}
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
fn version(&self) -> pgp::types::KeyVersion {
match self {
Self::Key(k) => k.version(),
Self::Subkey(k) => k.version(),
}
}
fn fingerprint(&self) -> pgp::types::Fingerprint {
impl KeyTrait for SignedPublicKeyOrSubkey<'_> {
fn fingerprint(&self) -> Vec<u8> {
match self {
Self::Key(k) => k.fingerprint(),
Self::Subkey(k) => k.fingerprint(),
@@ -69,26 +62,14 @@ impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
Self::Subkey(k) => k.algorithm(),
}
}
}
fn created_at(&self) -> &chrono::DateTime<chrono::Utc> {
match self {
Self::Key(k) => k.created_at(),
Self::Subkey(k) => k.created_at(),
}
}
fn expiration(&self) -> Option<u16> {
match self {
Self::Key(k) => k.expiration(),
Self::Subkey(k) => k.expiration(),
}
}
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
fn verify_signature(
&self,
hash: HashAlgorithm,
data: &[u8],
sig: &SignatureBytes,
sig: &[Mpi],
) -> pgp::errors::Result<()> {
match self {
Self::Key(k) => k.verify_signature(hash, data, sig),
@@ -98,27 +79,19 @@ impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
fn encrypt<R: Rng + CryptoRng>(
&self,
rng: R,
rng: &mut R,
plain: &[u8],
typ: pgp::types::EskType,
) -> pgp::errors::Result<pgp::types::PkeskBytes> {
) -> pgp::errors::Result<Vec<Mpi>> {
match self {
Self::Key(k) => k.encrypt(rng, plain, typ),
Self::Subkey(k) => k.encrypt(rng, plain, typ),
Self::Key(k) => k.encrypt(rng, plain),
Self::Subkey(k) => k.encrypt(rng, plain),
}
}
fn serialize_for_hashing(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
fn to_writer_old(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
match self {
Self::Key(k) => k.serialize_for_hashing(writer),
Self::Subkey(k) => k.serialize_for_hashing(writer),
}
}
fn public_params(&self) -> &pgp::types::PublicParams {
match self {
Self::Key(k) => k.public_params(),
Self::Subkey(k) => k.public_params(),
Self::Key(k) => k.to_writer_old(writer),
Self::Subkey(k) => k.to_writer_old(writer),
}
}
}
@@ -187,10 +160,9 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
let (signing_key_type, encryption_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
KeyGenType::Ed25519 | KeyGenType::Default => (
PgpKeyType::EdDSALegacy,
PgpKeyType::ECDH(ECCCurve::Curve25519),
),
KeyGenType::Ed25519 | KeyGenType::Default => {
(PgpKeyType::EdDSA, PgpKeyType::ECDH(ECCCurve::Curve25519))
}
};
let user_id = format!("<{addr}>");
@@ -227,11 +199,10 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
.build()
.context("failed to build key parameters")?;
let mut rng = thread_rng();
let secret_key = key_params
.generate(&mut rng)
.generate()
.context("failed to generate the key")?
.sign(&mut rng, || "".into())
.sign(|| "".into())
.context("failed to sign secret key")?;
secret_key
.verify()
@@ -290,19 +261,15 @@ pub async fn pk_encrypt(
let mut rng = thread_rng();
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
let signed_msg = lit_msg.sign(&mut rng, skey, || "".into(), HASH_ALGORITHM)?;
let signed_msg = lit_msg.sign(skey, || "".into(), HASH_ALGORITHM)?;
let compressed_msg = if compress {
signed_msg.compress(CompressionAlgorithm::ZLIB)?
} else {
signed_msg
};
compressed_msg.encrypt_to_keys_seipdv1(
&mut rng,
SYMMETRIC_KEY_ALGORITHM,
&pkeys_refs,
)?
compressed_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
} else {
lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
};
let encoded_msg = encrypted_msg.to_armored_string(Default::default())?;
@@ -317,9 +284,7 @@ pub fn pk_calc_signature(
plain: &[u8],
private_key_for_signing: &SignedSecretKey,
) -> Result<String> {
let mut rng = thread_rng();
let msg = Message::new_literal_bytes("", plain).sign(
&mut rng,
private_key_for_signing,
|| "".into(),
HASH_ALGORITHM,
@@ -332,43 +297,43 @@ pub fn pk_calc_signature(
///
/// Receiver private keys are provided in
/// `private_keys_for_decryption`.
///
/// Returns decrypted message and fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
#[allow(clippy::implicit_hasher)]
pub fn pk_decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
) -> Result<pgp::composed::Message> {
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
let cursor = Cursor::new(ctext);
let (msg, _headers) = Message::from_armor_single(cursor)?;
let (msg, _) = Message::from_armor_single(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let (msg, _key_ids) = msg.decrypt(|| "".into(), &skeys[..])?;
let (msg, _) = msg.decrypt(|| "".into(), &skeys[..])?;
// get_content() will decompress the message if needed,
// but this avoids decompressing it again to check signatures
let msg = msg.decompress()?;
Ok(msg)
}
let content = match msg.get_content()? {
Some(content) => content,
None => bail!("The decrypted message is empty"),
};
/// Returns fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
///
/// If the message is wrongly signed, HashSet will be empty.
pub fn valid_signature_fingerprints(
msg: &pgp::composed::Message,
public_keys_for_validation: &[SignedPublicKey],
) -> Result<HashSet<Fingerprint>> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in public_keys_for_validation {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = pkey.dc_fingerprint();
let fp = DcKey::fingerprint(pkey);
ret_signature_fingerprints.insert(fp);
}
}
}
Ok(ret_signature_fingerprints)
Ok((content, ret_signature_fingerprints))
}
/// Validates detached signature.
@@ -390,7 +355,7 @@ pub fn pk_validate(
for pkey in public_keys_for_validation {
if standalone_signature.verify(pkey, content).is_ok() {
let fp = pkey.dc_fingerprint();
let fp = DcKey::fingerprint(pkey);
ret.insert(fp);
}
}
@@ -405,12 +370,8 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
tokio::task::spawn_blocking(move || {
let mut rng = thread_rng();
let s2k = StringToKey::new_default(&mut rng);
let msg = lit_msg.encrypt_with_password_seipdv1(
&mut rng,
s2k,
SYMMETRIC_KEY_ALGORITHM,
|| passphrase,
)?;
let msg =
lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?;
let encoded_msg = msg.to_armored_string(Default::default())?;
@@ -446,18 +407,6 @@ mod tests {
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
fn pk_decrypt_and_validate(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(pgp::composed::Message, HashSet<Fingerprint>)> {
let msg = pk_decrypt(ctext, private_keys_for_decryption)?;
let ret_signature_fingerprints =
valid_signature_fingerprints(&msg, public_keys_for_validation)?;
Ok((msg, ret_signature_fingerprints))
}
#[test]
fn test_split_armored_data_1() {
let (typ, _headers, base64) = split_armored_data(
@@ -585,35 +534,34 @@ mod tests {
// Check decrypting as Alice
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_no_sig_check() {
let keyring = vec![KEYS.alice_secret.clone()];
let (msg, valid_signatures) =
pk_decrypt_and_validate(ctext_signed().await.as_bytes().to_vec(), &keyring, &[])
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
let (plain, valid_signatures) =
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -622,26 +570,26 @@ mod tests {
// The validation does not have the public key of the signer.
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.bob_public.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
let (plain, valid_signatures) = pk_decrypt(
ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring,
&[],
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
}

View File

@@ -2,7 +2,7 @@
use once_cell::sync::Lazy;
use crate::simplify::remove_message_footer;
use crate::simplify::split_lines;
/// Plaintext message body together with format=flowed attributes.
#[derive(Debug)]
@@ -32,8 +32,7 @@ impl PlainText {
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});
let lines: Vec<&str> = self.text.lines().collect();
let (lines, _footer) = remove_message_footer(&lines);
let lines = split_lines(&self.text);
let mut ret = r#"<!DOCTYPE html>
<html><head>
@@ -137,28 +136,7 @@ line 1<br/>
line 2<br/>
line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/>
<a href="http://link-at-start-of-line.org">http://link-at-start-of-line.org</a><br/>
</body></html>
"#
);
}
#[test]
fn test_plain_remove_signature() {
let html = PlainText {
text: "Foo\nbar\n-- \nSignature here".to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
Foo<br/>
bar<br/>
<br/>
</body></html>
"#
);

View File

@@ -288,6 +288,8 @@ pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::test_utils::TestContext;

View File

@@ -1,16 +1,10 @@
use std::sync::atomic::Ordering;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use base64::Engine as _;
use pgp::crypto::aead::AeadAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::ser::Serialize;
use rand::thread_rng;
use anyhow::Result;
use tokio::sync::RwLock;
use crate::context::Context;
use crate::key::DcKey;
/// Manages subscription to Apple Push Notification services.
///
@@ -30,85 +24,20 @@ pub struct PushSubscriber {
inner: Arc<RwLock<PushSubscriberState>>,
}
/// The key was generated with
/// `rsop generate-key --profile rfc9580`
/// and public key was extracted with `rsop extract-cert`.
const NOTIFIERS_PUBLIC_KEY: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----
xioGZ03cdhsAAAAg6PasQQylEuWAp9N5PXN93rqjZdqOqN3s9RJEU/K8FZzCsAYf
GwoAAABBBQJnTdx2AhsDAh4JCAsJCAcKDQwLBRUKCQgLAhYCIiEGiJJktnCmEtXa
qsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAAUfgg/sg0sR2mytzADFBpNAaY0Hyu
aru8ics3eUkeNn2ziL4ZsIMx+4mcM5POvD0PG9LtH8Rz/y9iItD0c2aoRBab7iri
/gDm6aQuj3xXgtAiXdaN9s+QPxR9gY/zG1t9iXgBzioGZ03cdhkAAAAgwJ0wQFsk
MGH4jklfK1fFhYoQZMjEFCRBIk+r1S+WaSDClQYYGwgAAAAsBQJnTdx2AhsMIiEG
iJJktnCmEtXaqsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAKCRCIkmS2cKYS1WdP
EFerccH2BoIPNbrxi6hwvxxy7G1mHg//ofD90fqmeY9xTfKMYl16bqQh4R1PiYd5
LMc5VqgXHgioqTYKbltlOtWC+HDt/PrymQsN4q/aEmsM
=5jvt
-----END PGP PUBLIC KEY BLOCK-----";
/// Pads the token with spaces.
///
/// This makes it impossible to tell
/// if the user is an Apple user with shorter tokens
/// or FCM user with longer tokens by the length of ciphertext.
fn pad_device_token(s: &str) -> String {
// 512 is larger than any token, tokens seen so far have not been larger than 200 bytes.
let expected_len: usize = 512;
let payload_len = s.len();
let padding_len = expected_len.saturating_sub(payload_len);
let padding = " ".repeat(padding_len);
let res = format!("{s}{padding}");
debug_assert_eq!(res.len(), expected_len);
res
}
/// Encrypts device token with OpenPGP.
///
/// The result is base64-encoded and not ASCII armored to avoid dealing with newlines.
pub(crate) fn encrypt_device_token(device_token: &str) -> Result<String> {
let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0;
let encryption_subkey = public_key
.public_subkeys
.first()
.context("No encryption subkey found")?;
let padded_device_token = pad_device_token(device_token);
let literal_message = pgp::composed::Message::new_literal("", &padded_device_token);
let mut rng = thread_rng();
let chunk_size = 8;
let encrypted_message = literal_message.encrypt_to_keys_seipdv2(
&mut rng,
SymmetricKeyAlgorithm::AES128,
AeadAlgorithm::Ocb,
chunk_size,
&[&encryption_subkey],
)?;
let encoded_message = encrypted_message.to_bytes()?;
Ok(format!(
"openpgp:{}",
base64::engine::general_purpose::STANDARD.encode(encoded_message)
))
}
impl PushSubscriber {
/// Creates new push notification subscriber.
pub(crate) fn new() -> Self {
Default::default()
}
/// Sets device token for Apple Push Notification service
/// or Firebase Cloud Messaging.
pub(crate) async fn set_device_token(&self, token: &str) {
/// Sets device token for Apple Push Notification service.
pub(crate) async fn set_device_token(&mut self, token: &str) {
self.inner.write().await.device_token = Some(token.to_string());
}
/// Retrieves device token.
///
/// The token is encrypted with OpenPGP.
///
/// Token may be not available if application is not running on Apple platform,
/// does not have Google Play services,
/// failed to register for remote notifications or is in the process of registering.
///
/// IMAP loop should periodically check if device token is available
@@ -192,37 +121,3 @@ impl Context {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_device_token() {
let push_subscriber = PushSubscriber::new();
assert_eq!(push_subscriber.device_token().await, None);
push_subscriber.set_device_token("some-token").await;
let device_token = push_subscriber.device_token().await.unwrap();
assert_eq!(device_token, "some-token");
}
#[test]
fn test_pad_device_token() {
let apple_token = "0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894";
assert_eq!(pad_device_token(apple_token).trim(), apple_token);
}
#[test]
fn test_encrypt_device_token() {
let fcm_token = encrypt_device_token("fcm-chat.delta:c67DVcpVQN2rJHiSszKNDW:APA91bErcJV2b8qG0IT4aiuCqw6Al0_SbydSuz3V0CHBR1X7Fp8YzyvlpxNZIOGYVDFKejZGE1YiGSaqxmkr9ds0DuALmZNDwqIhuZWGKKrs3r7DTSkQ9MQ").unwrap();
let fcm_beta_token = encrypt_device_token("fcm-chat.delta.beta:chu-GhZCTLyzq1XseJp3na:APA91bFlsfDawdszWTyOLbxBy7KeRCrYM-SBFqutebF5ix0EZKMuCFUT_Y7R7Ex_eTQG_LbOu3Ky_z5UlTMJtI7ufpIp5wEvsFmVzQcOo3YhrUpbiSVGIlk").unwrap();
let apple_token = encrypt_device_token(
"0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894",
)
.unwrap();
assert_eq!(fcm_token.len(), fcm_beta_token.len());
assert_eq!(apple_token.len(), fcm_token.len());
}
}

View File

@@ -112,7 +112,7 @@ pub enum Qr {
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Iroh node address.
node_addr: iroh::NodeAddr,
node_addr: iroh_net::NodeAddr,
/// Authentication token.
auth_token: String,
@@ -270,7 +270,6 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
/// The function should be called after a QR code is scanned.
/// The function takes the raw text scanned and checks what can be done with it.
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
let qr = qr.trim();
let qrcode = if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
.await
@@ -368,10 +367,9 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
#[allow(clippy::indexing_slicing)]
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let payload = qr
.get(OPENPGP4FPR_SCHEME.len()..)
.context("Invalid OPENPGP4FPR scheme")?;
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
@@ -448,7 +446,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledSecurejoinQrScan)
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan)
.await
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
@@ -548,7 +546,7 @@ async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<
fn decode_account(qr: &str) -> Result<Qr> {
let payload = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("Invalid DCACCOUNT payload")?;
.context("invalid DCACCOUNT payload")?;
let url = url::Url::parse(payload).context("Invalid account URL")?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::Account {
@@ -566,7 +564,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
let payload = qr
.get(DCWEBRTC_SCHEME.len()..)
.context("Invalid DCWEBRTC payload")?;
.context("invalid DCWEBRTC payload")?;
let (_type, url) = Message::parse_webrtc_instance(payload);
let url = url::Url::parse(&url).context("Invalid WebRTC instance")?;
@@ -639,12 +637,12 @@ fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
fn decode_backup2(qr: &str) -> Result<Qr> {
let payload = qr
.strip_prefix(DCBACKUP2_SCHEME)
.ok_or_else(|| anyhow!("Invalid DCBACKUP2 scheme"))?;
.ok_or_else(|| anyhow!("invalid DCBACKUP scheme"))?;
let (auth_token, node_addr) = payload
.split_once('&')
.context("Backup QR code has no separator")?;
let auth_token = auth_token.to_string();
let node_addr = serde_json::from_str::<iroh::NodeAddr>(node_addr)
let node_addr = serde_json::from_str::<iroh_net::NodeAddr>(node_addr)
.context("Invalid node addr in backup QR code")?;
Ok(Qr::Backup2 {
@@ -670,10 +668,9 @@ struct CreateAccountErrorResponse {
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
/// on success, a configure::configure() should be able to log in to the account
#[allow(clippy::indexing_slicing)]
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
let url_str = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("Invalid DCACCOUNT scheme")?;
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
if !url_str.starts_with(HTTPS_SCHEME) {
bail!("DCACCOUNT QR codes must use HTTPS scheme");
@@ -803,12 +800,15 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
/// Extract address for the mailto scheme.
///
/// Scheme: `mailto:addr...?subject=...&body=..`
#[allow(clippy::indexing_slicing)]
async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
let payload = qr
.get(MAILTO_SCHEME.len()..)
.context("Invalid mailto: scheme")?;
let payload = &qr[MAILTO_SCHEME.len()..];
let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
let (addr, query) = if let Some(query_index) = payload.find('?') {
(&payload[..query_index], &payload[query_index + 1..])
} else {
(payload, "")
};
let param: BTreeMap<&str, &str> = query
.split('&')
@@ -855,12 +855,16 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
/// Extract address for the smtp scheme.
///
/// Scheme: `SMTP:addr...:subject...:body...`
#[allow(clippy::indexing_slicing)]
async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
let payload = &qr[SMTP_SCHEME.len()..];
let addr = if let Some(query_index) = payload.find(':') {
&payload[..query_index]
} else {
bail!("Invalid SMTP found");
};
let (addr, _rest) = payload
.split_once(':')
.context("Invalid SMTP scheme payload")?;
let addr = normalize_address(addr)?;
let name = "";
Qr::from_address(context, name, &addr, None).await
@@ -871,13 +875,14 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
///
/// There may or may not be linebreaks after the fields.
#[allow(clippy::indexing_slicing)]
async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
// we ignore this case.
let addr = if let Some(to_index) = qr.find("TO:") {
let addr = qr.get(to_index + 3..).unwrap_or_default().trim();
let addr = qr[to_index + 3..].trim();
if let Some(semi_index) = addr.find(';') {
addr.get(..semi_index).unwrap_or_default().trim()
addr[..semi_index].trim()
} else {
addr
}
@@ -898,6 +903,7 @@ static VCARD_EMAIL_RE: Lazy<regex::Regex> =
/// Extract address for the vcard scheme.
///
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
#[allow(clippy::indexing_slicing)]
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
let name = VCARD_NAME_RE
.captures(qr)
@@ -909,8 +915,8 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
})
.unwrap_or_default();
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
normalize_address(cap.as_str().trim())?
let addr = if let Some(caps) = VCARD_EMAIL_RE.captures(qr) {
normalize_address(caps[2].trim())?
} else {
bail!("Bad e-mail address");
};
@@ -995,17 +1001,6 @@ mod tests {
}
);
// Test that QR code whitespace is stripped.
// Users can copy-paste QR code contents and "scan"
// from the clipboard.
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
assert_eq!(
qr,
Qr::Url {
url: "http://www.hello.com/hello".to_string(),
}
);
Ok(())
}
@@ -1275,8 +1270,7 @@ mod tests {
if let Qr::AskVerifyContact { contact_id, .. } = qr {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "cli@deltachat.de");
assert_eq!(contact.get_authname(), "Jörn P. P.");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_name(), "Jörn P. P.");
} else {
bail!("Wrong QR code type");
}
@@ -1291,7 +1285,6 @@ mod tests {
if let Qr::AskVerifyContact { contact_id, .. } = qr {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "cli@deltachat.de");
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
} else {
bail!("Wrong QR code type");
@@ -1314,7 +1307,7 @@ mod tests {
last_seen_autocrypt: 1,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: None,
gossip_timestamp: 0,
gossip_key_fingerprint: None,
@@ -1345,10 +1338,7 @@ mod tests {
let qr = check_qr(
&ctx.ctx,
&format!(
"OPENPGP4FPR:{}#a=alice@example.org",
pub_key.dc_fingerprint()
),
&format!("OPENPGP4FPR:{}#a=alice@example.org", pub_key.fingerprint()),
)
.await?;
if let Qr::FprOk { contact_id, .. } = qr {
@@ -1755,9 +1745,7 @@ mod tests {
);
// Test URL without port.
//
// Also check that whitespace is trimmed.
let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await;
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
assert_eq!(

View File

@@ -11,7 +11,7 @@ use crate::config::Config;
use crate::context::Context;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::session::Session as ImapSession;
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::tools::{self, time_elapsed};
use crate::{stock_str, EventType};
@@ -142,8 +142,8 @@ impl Context {
Some(&highest.to_string()),
)
.await?;
let mut msg =
Message::new_text(stock_str::quota_exceeding(self, highest).await);
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::quota_exceeding(self, highest).await;
add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
self.set_config_internal(Config::QuotaExceeding, None)

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