mirror of
https://github.com/chatmail/core.git
synced 2026-10-08 22:20:17 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189a093e02 |
@@ -249,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
|
||||
@@ -314,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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"
|
||||
|
||||
+4
-207
@@ -1,205 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
@@ -4591,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
|
||||
@@ -5335,10 +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
|
||||
|
||||
+165
-101
@@ -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 commited unrelated changes and files.
|
||||
|
||||
Push the necessary fixup commits or force-push to your branch if needed.
|
||||
|
||||
6. **Ask for review.**
|
||||
|
||||
Use built-in GitHub feature to request a review from suggested reviewers.
|
||||
|
||||
If you do not have write access to the repository, ask for review in the comments.
|
||||
|
||||
7. **Merge the PR.**
|
||||
|
||||
Once a PR has an approval and passes CI, it can be merged.
|
||||
|
||||
PRs from a branch created in the main repository,
|
||||
i.e. authored by those who have write access, are merged by their authors.
|
||||
|
||||
This is to ensure that PRs are merged as intended by the author,
|
||||
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
|
||||
|
||||
If you have multiple changes in one PR, do a rebase merge.
|
||||
Otherwise, you should usually do a squash merge.
|
||||
|
||||
If PR author does not have write access to the repository,
|
||||
maintainers who reviewed the PR can merge it.
|
||||
|
||||
If you do not have access to the repository and created a PR from a fork,
|
||||
ask the maintainers to merge the PR and say how it should be merged.
|
||||
|
||||
## Other ways to contribute
|
||||
|
||||
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
||||
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).
|
||||
|
||||
Generated
+254
-550
File diff suppressed because it is too large
Load Diff
+23
-16
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.151.1"
|
||||
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.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,11 +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.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.28.1", default-features = false }
|
||||
kamadak-exif = "0.6.0"
|
||||
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"
|
||||
@@ -76,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.14", 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"
|
||||
@@ -172,13 +172,13 @@ 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.4.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"
|
||||
@@ -187,7 +187,14 @@ serde = "1.0"
|
||||
serde_json = "1"
|
||||
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"
|
||||
|
||||
@@ -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.
@@ -15,8 +15,7 @@
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.151.1"
|
||||
version = "1.148.4"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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().
|
||||
*
|
||||
@@ -1154,13 +1149,7 @@ 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 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
|
||||
@@ -2547,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
|
||||
@@ -4203,7 +4190,6 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* true if the Webxdc should get full internet access, including Webrtc.
|
||||
* currently, this is only true for encrypted Webxdc's in the self chat
|
||||
* that have requested internet access in the manifest.
|
||||
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
@@ -4518,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().
|
||||
@@ -5736,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
|
||||
|
||||
@@ -6104,33 +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 contact_id ID of the contact sending.
|
||||
* @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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
@@ -603,8 +602,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
EventType::IncomingReaction { contact_id, .. }
|
||||
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -683,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, .. }
|
||||
@@ -778,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"),
|
||||
@@ -3687,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() {
|
||||
@@ -4885,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]
|
||||
@@ -4899,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
|
||||
}
|
||||
|
||||
@@ -4922,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:#}."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.151.1"
|
||||
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"
|
||||
|
||||
@@ -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,
|
||||
@@ -2004,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)
|
||||
}
|
||||
@@ -2130,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())
|
||||
@@ -2173,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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -106,14 +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,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -327,15 +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,
|
||||
} => IncomingWebxdcNotify {
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
text,
|
||||
},
|
||||
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
@@ -426,9 +409,6 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,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
|
||||
@@ -185,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(),
|
||||
@@ -246,8 +244,7 @@ impl MessageObject {
|
||||
reactions,
|
||||
|
||||
vcard_contact: vcard_contacts.first().cloned(),
|
||||
};
|
||||
Ok(Some(message_object))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,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,
|
||||
@@ -533,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")?,
|
||||
|
||||
@@ -6,161 +6,87 @@ use typescript_type_def::TypeDef;
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum QrObject {
|
||||
/// Ask the user whether to verify the contact.
|
||||
///
|
||||
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
|
||||
AskVerifyContact {
|
||||
/// ID of the contact.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user whether to join the group.
|
||||
AskVerifyGroup {
|
||||
/// Group name.
|
||||
grpname: String,
|
||||
/// Group ID.
|
||||
grpid: String,
|
||||
/// ID of the contact.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Contact fingerprint is verified.
|
||||
///
|
||||
/// Ask the user if they want to start chatting.
|
||||
FprOk {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
},
|
||||
/// Scanned fingerprint does not match the last seen fingerprint.
|
||||
FprMismatch {
|
||||
/// Contact ID.
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
/// The scanned QR code contains a fingerprint but no e-mail address.
|
||||
FprWithoutAddr {
|
||||
/// Key fingerprint.
|
||||
fingerprint: String,
|
||||
},
|
||||
/// Ask the user if they want to create an account on the given domain.
|
||||
Account {
|
||||
/// Server domain name.
|
||||
domain: String,
|
||||
},
|
||||
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
|
||||
Backup2 {
|
||||
/// Authentication token.
|
||||
auth_token: String,
|
||||
/// Iroh node address.
|
||||
|
||||
node_addr: String,
|
||||
},
|
||||
/// Ask the user if they want to use the given service for video chats.
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
/// Ask the user if they want to use the given proxy.
|
||||
///
|
||||
/// Note that HTTP(S) URLs without a path
|
||||
/// and query parameters are treated as HTTP(S) proxy URL.
|
||||
/// UI may want to still offer to open the URL
|
||||
/// in the browser if QR code contents
|
||||
/// starts with `http://` or `https://`
|
||||
/// and the QR code was not scanned from
|
||||
/// the proxy configuration screen.
|
||||
Proxy {
|
||||
/// Proxy URL.
|
||||
///
|
||||
/// This is the URL that is going to be added.
|
||||
url: String,
|
||||
/// Host extracted from the URL to display in the UI.
|
||||
host: String,
|
||||
/// Port extracted from the URL to display in the UI.
|
||||
port: u16,
|
||||
},
|
||||
/// Contact address is scanned.
|
||||
///
|
||||
/// Optionally, a draft message could be provided.
|
||||
/// Ask the user if they want to start chatting.
|
||||
Addr {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Draft message.
|
||||
draft: Option<String>,
|
||||
},
|
||||
/// URL scanned.
|
||||
///
|
||||
/// Ask the user if they want to open a browser or copy the URL to clipboard.
|
||||
Url { url: String },
|
||||
/// Text scanned.
|
||||
///
|
||||
/// Ask the user if they want to copy the text to clipboard.
|
||||
Text { text: String },
|
||||
/// Ask the user if they want to withdraw their own QR code.
|
||||
Url {
|
||||
url: String,
|
||||
},
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
WithdrawVerifyContact {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to withdraw their own group invite QR code.
|
||||
WithdrawVerifyGroup {
|
||||
/// Group name.
|
||||
grpname: String,
|
||||
/// Group ID.
|
||||
grpid: String,
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to revive their own QR code.
|
||||
ReviveVerifyContact {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to revive their own group invite QR code.
|
||||
ReviveVerifyGroup {
|
||||
/// Contact ID.
|
||||
grpname: String,
|
||||
/// Group ID.
|
||||
grpid: String,
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// `dclogin:` scheme parameters.
|
||||
///
|
||||
/// Ask the user if they want to login with the email address.
|
||||
Login { address: String },
|
||||
Login {
|
||||
address: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
@@ -215,6 +141,7 @@ impl From<Qr> for QrObject {
|
||||
auth_token,
|
||||
} => QrObject::Backup2 {
|
||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
||||
|
||||
auth_token,
|
||||
},
|
||||
Qr::WebrtcInstance {
|
||||
|
||||
@@ -35,8 +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,
|
||||
}
|
||||
|
||||
impl WebxdcMessageInfo {
|
||||
@@ -52,7 +50,6 @@ impl WebxdcMessageInfo {
|
||||
summary,
|
||||
source_code_url,
|
||||
internet_access,
|
||||
self_addr,
|
||||
} = message.get_webxdc_info(context).await?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -62,7 +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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
pub mod api;
|
||||
pub use yerpc;
|
||||
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.151.1"
|
||||
"version": "1.148.4"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.151.1"
|
||||
version = "1.148.4"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
@@ -1004,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()
|
||||
@@ -1027,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" => {
|
||||
|
||||
@@ -25,8 +25,7 @@ $ pip install .
|
||||
## Testing
|
||||
|
||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
2. Install tox `pip install -U tox`
|
||||
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.151.1"
|
||||
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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
+13
-10
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -24,7 +24,6 @@ def test_webxdc(acfactory) -> None:
|
||||
"name": "Chess Board",
|
||||
"sourceCodeUrl": None,
|
||||
"summary": None,
|
||||
"selfAddr": webxdc_info["selfAddr"],
|
||||
}
|
||||
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
|
||||
@@ -16,7 +16,6 @@ deps =
|
||||
pytest
|
||||
pytest-timeout
|
||||
pytest-xdist
|
||||
imap-tools
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.151.1"
|
||||
version = "1.148.4"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.151.1"
|
||||
"version": "1.148.4"
|
||||
}
|
||||
|
||||
@@ -11,13 +11,6 @@ ignore = [
|
||||
|
||||
# Unmaintained encoding
|
||||
"RUSTSEC-2021-0153",
|
||||
|
||||
# Unmaintained proc-macro-error
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2024-0370>
|
||||
"RUSTSEC-2024-0370",
|
||||
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -33,14 +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 = "h2", version = "0.3.26" },
|
||||
{ name = "http-body", version = "0.4.6" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hyper", version = "0.14.28" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ 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" },
|
||||
@@ -51,7 +43,6 @@ skip = [
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "toml_edit", version = "0.19.15" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.52" },
|
||||
@@ -64,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" },
|
||||
]
|
||||
|
||||
|
||||
Generated
+73
-38
@@ -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",
|
||||
|
||||
@@ -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
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
Generated
+1561
-922
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
DC_CERTCK_ACCEPT_INVALID: 2,
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
|
||||
DC_CERTCK_AUTO: 0,
|
||||
DC_CERTCK_STRICT: 1,
|
||||
@@ -52,7 +51,6 @@ module.exports = {
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
|
||||
DC_EVENT_INCOMING_REACTION: 2002,
|
||||
DC_EVENT_INCOMING_WEBXDC_NOTIFY: 2003,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
|
||||
@@ -17,7 +17,6 @@ module.exports = {
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
|
||||
@@ -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,
|
||||
@@ -52,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,
|
||||
@@ -326,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',
|
||||
|
||||
+1
-1
@@ -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.1"
|
||||
"version": "1.148.4"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.151.1"
|
||||
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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2024-11-24
|
||||
2024-10-24
|
||||
@@ -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]
|
||||
|
||||
+18
-45
@@ -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;
|
||||
@@ -302,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.
|
||||
@@ -351,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.
|
||||
@@ -371,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(())
|
||||
}
|
||||
|
||||
+14
-8
@@ -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(())
|
||||
|
||||
+107
-90
@@ -765,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();
|
||||
@@ -789,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);
|
||||
@@ -914,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2086,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 {
|
||||
@@ -2139,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,
|
||||
@@ -2190,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,
|
||||
@@ -3100,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
|
||||
}
|
||||
|
||||
@@ -4500,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,
|
||||
@@ -4774,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();
|
||||
@@ -4788,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());
|
||||
|
||||
@@ -4806,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);
|
||||
|
||||
@@ -4819,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());
|
||||
|
||||
@@ -4865,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 {
|
||||
@@ -4898,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?;
|
||||
@@ -4951,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());
|
||||
@@ -5366,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(())
|
||||
}
|
||||
@@ -5461,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());
|
||||
@@ -5494,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());
|
||||
@@ -5546,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());
|
||||
@@ -5563,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();
|
||||
@@ -5580,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();
|
||||
@@ -5603,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());
|
||||
|
||||
@@ -5614,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();
|
||||
@@ -5646,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
|
||||
@@ -5946,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
|
||||
@@ -6023,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);
|
||||
@@ -6662,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;
|
||||
|
||||
@@ -6713,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;
|
||||
@@ -6796,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"));
|
||||
@@ -7691,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(())
|
||||
}
|
||||
}
|
||||
|
||||
+12
-35
@@ -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;
|
||||
|
||||
@@ -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,7 +433,6 @@ pub enum Config {
|
||||
WebxdcIntegration,
|
||||
|
||||
/// Enable webxdc realtime features.
|
||||
#[strum(props(default = "1"))]
|
||||
WebxdcRealtimeEnabled,
|
||||
}
|
||||
|
||||
|
||||
+18
-20
@@ -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,7 +31,7 @@ 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;
|
||||
@@ -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, ¶m).await?;
|
||||
|
||||
let configured_param_res = configure(self, ¶m).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),
|
||||
};
|
||||
@@ -499,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(¶m.addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
@@ -611,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;
|
||||
|
||||
@@ -272,6 +272,8 @@ pub(crate) async fn moz_autoconfigure(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -215,6 +215,8 @@ pub(crate) async fn outlk_autodiscover(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
+4
-99
@@ -143,43 +143,6 @@ impl ContactId {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns contact adress.
|
||||
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,59 +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(())
|
||||
}
|
||||
}
|
||||
|
||||
+11
-59
@@ -10,7 +10,6 @@ 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, OnceCell, RwLock};
|
||||
@@ -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;
|
||||
@@ -472,14 +471,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -790,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}>"),
|
||||
};
|
||||
|
||||
@@ -999,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(),
|
||||
@@ -1186,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?;
|
||||
|
||||
@@ -1792,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;
|
||||
@@ -1898,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?;
|
||||
}
|
||||
@@ -2072,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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+145
-7
@@ -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
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+3
-3
@@ -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,
|
||||
|
||||
+6
-5
@@ -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;
|
||||
|
||||
@@ -107,18 +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,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
|
||||
+8
-3
@@ -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 < > and & but also " and ' :)<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
@@ -521,7 +525,8 @@ test some special html-characters as < > and & but also " 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();
|
||||
|
||||
+50
-82
@@ -36,7 +36,7 @@ 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;
|
||||
@@ -89,7 +89,7 @@ pub(crate) struct Imap {
|
||||
|
||||
oauth2: bool,
|
||||
|
||||
authentication_failed_once: bool,
|
||||
login_failed_once: bool,
|
||||
|
||||
pub(crate) connectivity: ConnectivityStore,
|
||||
|
||||
@@ -254,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,
|
||||
@@ -290,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)
|
||||
@@ -402,7 +398,7 @@ 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
|
||||
@@ -416,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,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;
|
||||
@@ -1202,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
|
||||
@@ -1213,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);
|
||||
@@ -1236,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
|
||||
@@ -1730,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(_) => {
|
||||
@@ -1752,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1916,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) {
|
||||
|
||||
+27
-5
@@ -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!(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-32
@@ -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() {
|
||||
@@ -461,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));
|
||||
@@ -504,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(())
|
||||
@@ -543,6 +552,10 @@ impl<W> ProgressWriter<W> {
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_inner(self) -> W {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> AsyncWrite for ProgressWriter<W>
|
||||
@@ -590,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)
|
||||
@@ -607,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.
|
||||
@@ -750,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);
|
||||
@@ -878,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();
|
||||
|
||||
+14
-9
@@ -36,13 +36,14 @@ use futures_lite::FutureExt;
|
||||
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};
|
||||
@@ -190,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.");
|
||||
@@ -200,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(())
|
||||
@@ -309,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.");
|
||||
@@ -368,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::*;
|
||||
@@ -382,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
|
||||
|
||||
+5
-13
@@ -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")
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+4
-1
@@ -290,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
|
||||
@@ -880,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;
|
||||
|
||||
+23
-143
@@ -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.
|
||||
@@ -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());
|
||||
|
||||
+14
-9
@@ -743,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:
|
||||
@@ -1984,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?;
|
||||
}
|
||||
@@ -2070,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();
|
||||
|
||||
@@ -2177,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
|
||||
@@ -2274,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()));
|
||||
@@ -2293,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");
|
||||
@@ -2359,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");
|
||||
@@ -2492,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();
|
||||
|
||||
+76
-197
@@ -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)
|
||||
.map_or(false, |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"
|
||||
@@ -1753,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();
|
||||
@@ -1915,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);
|
||||
}
|
||||
@@ -2303,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?;
|
||||
@@ -2362,6 +2289,8 @@ async fn ndn_maybe_add_info_msg(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use super::*;
|
||||
@@ -3669,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()
|
||||
);
|
||||
@@ -3694,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);
|
||||
@@ -3706,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;
|
||||
@@ -4095,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(())
|
||||
@@ -4167,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(())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
+28
-22
@@ -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());
|
||||
|
||||
|
||||
+33
-46
@@ -314,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
|
||||
@@ -590,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);
|
||||
@@ -728,6 +698,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();
|
||||
|
||||
assert!(alice
|
||||
.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await
|
||||
@@ -885,6 +866,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);
|
||||
@@ -953,11 +945,6 @@ 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
|
||||
|
||||
+23
-21
@@ -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);
|
||||
@@ -766,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(());
|
||||
};
|
||||
|
||||
@@ -814,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(());
|
||||
@@ -831,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))
|
||||
@@ -888,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,
|
||||
@@ -913,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");
|
||||
@@ -932,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,
|
||||
@@ -969,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,
|
||||
|
||||
+52
-104
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-25
@@ -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>
|
||||
"#
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ impl PushSubscriber {
|
||||
}
|
||||
|
||||
/// Sets device token for Apple Push Notification service.
|
||||
pub(crate) async fn set_device_token(&self, token: &str) {
|
||||
pub(crate) async fn set_device_token(&mut self, token: &str) {
|
||||
self.inner.write().await.device_token = Some(token.to_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,7 +637,7 @@ fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
fn decode_backup2(qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.strip_prefix(DCBACKUP2_SCHEME)
|
||||
.ok_or_else(|| anyhow!("Invalid DCBACKUP2 scheme"))?;
|
||||
.ok_or_else(|| anyhow!("invalid DCBACKUP scheme"))?;
|
||||
let (auth_token, node_addr) = payload
|
||||
.split_once('&')
|
||||
.context("Backup QR code has no separator")?;
|
||||
@@ -670,10 +668,9 @@ struct CreateAccountErrorResponse {
|
||||
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
|
||||
/// download additional information from the contained url and set the parameters.
|
||||
/// on success, a configure::configure() should be able to log in to the account
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
let url_str = qr
|
||||
.get(DCACCOUNT_SCHEME.len()..)
|
||||
.context("Invalid DCACCOUNT scheme")?;
|
||||
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
if !url_str.starts_with(HTTPS_SCHEME) {
|
||||
bail!("DCACCOUNT QR codes must use HTTPS scheme");
|
||||
@@ -803,12 +800,15 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
/// Extract address for the mailto scheme.
|
||||
///
|
||||
/// Scheme: `mailto:addr...?subject=...&body=..`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(MAILTO_SCHEME.len()..)
|
||||
.context("Invalid mailto: scheme")?;
|
||||
let payload = &qr[MAILTO_SCHEME.len()..];
|
||||
|
||||
let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
|
||||
let (addr, query) = if let Some(query_index) = payload.find('?') {
|
||||
(&payload[..query_index], &payload[query_index + 1..])
|
||||
} else {
|
||||
(payload, "")
|
||||
};
|
||||
|
||||
let param: BTreeMap<&str, &str> = query
|
||||
.split('&')
|
||||
@@ -855,12 +855,16 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
/// Extract address for the smtp scheme.
|
||||
///
|
||||
/// Scheme: `SMTP:addr...:subject...:body...`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
|
||||
let payload = &qr[SMTP_SCHEME.len()..];
|
||||
|
||||
let addr = if let Some(query_index) = payload.find(':') {
|
||||
&payload[..query_index]
|
||||
} else {
|
||||
bail!("Invalid SMTP found");
|
||||
};
|
||||
|
||||
let (addr, _rest) = payload
|
||||
.split_once(':')
|
||||
.context("Invalid SMTP scheme payload")?;
|
||||
let addr = normalize_address(addr)?;
|
||||
let name = "";
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
@@ -871,13 +875,14 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
|
||||
///
|
||||
/// There may or may not be linebreaks after the fields.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
|
||||
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
|
||||
// we ignore this case.
|
||||
let addr = if let Some(to_index) = qr.find("TO:") {
|
||||
let addr = qr.get(to_index + 3..).unwrap_or_default().trim();
|
||||
let addr = qr[to_index + 3..].trim();
|
||||
if let Some(semi_index) = addr.find(';') {
|
||||
addr.get(..semi_index).unwrap_or_default().trim()
|
||||
addr[..semi_index].trim()
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
@@ -898,6 +903,7 @@ static VCARD_EMAIL_RE: Lazy<regex::Regex> =
|
||||
/// Extract address for the vcard scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let name = VCARD_NAME_RE
|
||||
.captures(qr)
|
||||
@@ -909,8 +915,8 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
|
||||
normalize_address(cap.as_str().trim())?
|
||||
let addr = if let Some(caps) = VCARD_EMAIL_RE.captures(qr) {
|
||||
normalize_address(caps[2].trim())?
|
||||
} else {
|
||||
bail!("Bad e-mail address");
|
||||
};
|
||||
@@ -995,17 +1001,6 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
// Test that QR code whitespace is stripped.
|
||||
// Users can copy-paste QR code contents and "scan"
|
||||
// from the clipboard.
|
||||
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+3
-2
@@ -26,7 +26,7 @@ use crate::chatlist_events;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::message::{rfc724_mid_exists, Message, MsgId};
|
||||
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
|
||||
use crate::param::Param;
|
||||
|
||||
/// A single reaction consisting of multiple emoji sequences.
|
||||
@@ -229,7 +229,8 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
|
||||
let chat_id = msg.chat_id;
|
||||
|
||||
let reaction: Reaction = reaction.into();
|
||||
let mut reaction_msg = Message::new_text(reaction.as_str().to_string());
|
||||
let mut reaction_msg = Message::new(Viewtype::Text);
|
||||
reaction_msg.text = reaction.as_str().to_string();
|
||||
reaction_msg.set_reaction();
|
||||
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
|
||||
reaction_msg.hidden = true;
|
||||
|
||||
+109
-110
@@ -6,7 +6,7 @@ use std::str::FromStr;
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress};
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mailparse::SingleInfo;
|
||||
use mailparse::{parse_mail, SingleInfo};
|
||||
use num_traits::FromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
@@ -30,7 +30,7 @@ use crate::message::{
|
||||
};
|
||||
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
|
||||
use crate::peer_channels::{get_iroh_topic_for_msg, insert_topic_stub, iroh_add_peer_for_topic};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::reaction::{set_msg_reaction, Reaction};
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
@@ -41,6 +41,7 @@ use crate::sync::Sync::*;
|
||||
use crate::tools::{self, buf_compress, remove_subject_prefix};
|
||||
use crate::{chatlist_events, location};
|
||||
use crate::{contact, imap};
|
||||
use iroh_net::NodeAddr;
|
||||
|
||||
/// This is the struct that is returned after receiving one email (aka MIME message).
|
||||
///
|
||||
@@ -73,13 +74,12 @@ pub struct ReceivedMsg {
|
||||
///
|
||||
/// This method returns errors on a failure to parse the mail or extract Message-ID. It's only used
|
||||
/// for tests and REPL tool, not actual message reception pipeline.
|
||||
#[cfg(any(test, feature = "internals"))]
|
||||
pub async fn receive_imf(
|
||||
context: &Context,
|
||||
imf_raw: &[u8],
|
||||
seen: bool,
|
||||
) -> Result<Option<ReceivedMsg>> {
|
||||
let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
|
||||
let mail = parse_mail(imf_raw).context("can't parse mail")?;
|
||||
let rfc724_mid =
|
||||
imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
|
||||
if let Some(download_limit) = context.download_limit().await? {
|
||||
@@ -106,7 +106,6 @@ pub async fn receive_imf(
|
||||
/// Emulates reception of a message from "INBOX".
|
||||
///
|
||||
/// Only used for tests and REPL tool, not actual message reception pipeline.
|
||||
#[cfg(any(test, feature = "internals"))]
|
||||
pub(crate) async fn receive_imf_from_inbox(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
@@ -203,7 +202,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
};
|
||||
|
||||
crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?;
|
||||
if let Some(peerstate) = &mime_parser.peerstate {
|
||||
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
|
||||
peerstate
|
||||
.handle_fingerprint_change(context, mime_parser.timestamp_sent)
|
||||
.await?;
|
||||
@@ -358,7 +357,8 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
// Peerstate could be updated by handling the Securejoin handshake.
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
mime_parser.decryption_info.peerstate =
|
||||
Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
} else {
|
||||
let to_id = to_ids.first().copied().unwrap_or_default();
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
@@ -394,7 +394,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
if verified_encryption == VerifiedEncryption::Verified
|
||||
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some()
|
||||
{
|
||||
if let Some(peerstate) = &mut mime_parser.peerstate {
|
||||
if let Some(peerstate) = &mut mime_parser.decryption_info.peerstate {
|
||||
// NOTE: it might be better to remember ID of the key
|
||||
// that we used to decrypt the message, but
|
||||
// it is unlikely that default key ever changes
|
||||
@@ -762,7 +762,6 @@ async fn add_parts(
|
||||
let state: MessageState;
|
||||
let mut hidden = false;
|
||||
let mut needs_delete_job = false;
|
||||
let mut restore_protection = false;
|
||||
|
||||
// if contact renaming is prevented (for mailinglists and bots),
|
||||
// we use name from From:-header as override name
|
||||
@@ -931,11 +930,15 @@ async fn add_parts(
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to create a normal chat
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
let create_blocked = match contact.is_blocked() {
|
||||
true => Blocked::Yes,
|
||||
false if is_bot => Blocked::Not,
|
||||
false => Blocked::Request,
|
||||
let create_blocked = if from_id == ContactId::SELF {
|
||||
Blocked::Not
|
||||
} else {
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
match contact.is_blocked() {
|
||||
true => Blocked::Yes,
|
||||
false if is_bot => Blocked::Not,
|
||||
false => Blocked::Request,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(chat) = test_normal_chat {
|
||||
@@ -1007,13 +1010,6 @@ async fn add_parts(
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if let Some(peerstate) = &mime_parser.peerstate {
|
||||
restore_protection = new_protection != ProtectionStatus::Protected
|
||||
&& peerstate.prefer_encrypt == EncryptPreference::Mutual
|
||||
// Check that the contact still has the Autocrypt key same as the
|
||||
// verified key, see also `Peerstate::is_using_verified_key()`.
|
||||
&& contact.is_verified(context).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1032,8 @@ async fn add_parts(
|
||||
state = MessageState::OutDelivered;
|
||||
to_id = to_ids.first().copied().unwrap_or_default();
|
||||
|
||||
let self_sent = to_ids.len() == 1 && to_ids.contains(&ContactId::SELF);
|
||||
let self_sent =
|
||||
from_id == ContactId::SELF && to_ids.len() == 1 && to_ids.contains(&ContactId::SELF);
|
||||
|
||||
if mime_parser.sync_items.is_some() && self_sent {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
@@ -1094,8 +1091,8 @@ async fn add_parts(
|
||||
.await?;
|
||||
let now = tools::time();
|
||||
let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
|
||||
let mut msg =
|
||||
Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await);
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = stock_str::cant_decrypt_outgoing_msgs(context).await;
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.log_err(context)
|
||||
@@ -1425,11 +1422,7 @@ async fn add_parts(
|
||||
if let Some(msg) = group_changes_msgs.1 {
|
||||
match &better_msg {
|
||||
None => better_msg = Some(msg),
|
||||
Some(_) => {
|
||||
if !msg.is_empty() {
|
||||
group_changes_msgs.0.push(msg)
|
||||
}
|
||||
}
|
||||
Some(_) => group_changes_msgs.0.push(msg),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1450,27 +1443,61 @@ async fn add_parts(
|
||||
|
||||
if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
|
||||
Some((instance_id, _ts_sent)) => {
|
||||
if let Err(err) =
|
||||
add_gossip_peer_from_header(context, instance_id, node_addr).await
|
||||
{
|
||||
warn!(context, "Failed to add iroh peer from header: {err:#}.");
|
||||
match serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address") {
|
||||
Ok(node_addr) => {
|
||||
info!(context, "Adding iroh peer with address {node_addr:?}.");
|
||||
match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
|
||||
Some((instance_id, _ts_sent)) => {
|
||||
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
|
||||
msg_id: instance_id,
|
||||
});
|
||||
if let Some(topic) =
|
||||
get_iroh_topic_for_msg(context, instance_id).await?
|
||||
{
|
||||
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?;
|
||||
if context
|
||||
.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await?
|
||||
{
|
||||
let iroh = context.get_or_try_init_peer_channel().await?;
|
||||
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
|
||||
}
|
||||
info!(context, "Added iroh peer to the topic of {instance_id}.");
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Could not add iroh peer because {instance_id} has no topic."
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot add iroh peer because WebXDC instance does not exist."
|
||||
);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot add iroh peer because the message has no In-Reply-To."
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot add iroh peer because WebXDC instance does not exist."
|
||||
);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot add iroh peer because the message has no In-Reply-To."
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Couldn't parse NodeAddr: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1512,9 +1539,6 @@ async fn add_parts(
|
||||
|
||||
let mut txt_raw = "".to_string();
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
|
||||
if better_msg.is_empty() && is_partial_download.is_none() {
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
}
|
||||
(better_msg, Viewtype::Text)
|
||||
} else {
|
||||
(&part.msg, part.typ)
|
||||
@@ -1577,7 +1601,7 @@ INSERT INTO msgs
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
|
||||
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
|
||||
type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
|
||||
type=excluded.type, msgrmsg=excluded.msgrmsg,
|
||||
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
|
||||
txt_raw=excluded.txt_raw, param=excluded.param,
|
||||
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
|
||||
@@ -1722,16 +1746,7 @@ RETURNING id
|
||||
// delete it.
|
||||
needs_delete_job = true;
|
||||
}
|
||||
if restore_protection {
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
mime_parser.timestamp_rcvd,
|
||||
Some(from_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(ReceivedMsg {
|
||||
chat_id,
|
||||
state,
|
||||
@@ -2086,11 +2101,8 @@ async fn create_group(
|
||||
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
///
|
||||
/// Returns `Vec` of group changes messages and, optionally, a better message to replace the
|
||||
/// original system message. If the better message is empty, the original system message should be
|
||||
/// just omitted.
|
||||
///
|
||||
/// * `is_partial_download` - whether the message is not fully downloaded.
|
||||
/// Optionally returns better message to replace the original system message.
|
||||
/// is_partial_download: whether the message is not fully downloaded.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn apply_group_changes(
|
||||
context: &Context,
|
||||
@@ -2192,47 +2204,39 @@ async fn apply_group_changes(
|
||||
|
||||
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
|
||||
if let Some(id) = removed_id {
|
||||
if allow_member_list_changes && chat_contacts.contains(&id) {
|
||||
better_msg = if id == from_id {
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
|
||||
better_msg = if removed_id == Some(from_id) {
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
|
||||
if removed_id.is_some() {
|
||||
if !allow_member_list_changes {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring removal of {removed_addr:?} from {chat_id}."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Removed {removed_addr:?} has no contact id.")
|
||||
}
|
||||
better_msg.get_or_insert_with(Default::default);
|
||||
if !allow_member_list_changes {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring removal of {removed_addr:?} from {chat_id}."
|
||||
);
|
||||
}
|
||||
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
if allow_member_list_changes {
|
||||
let is_new_member;
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
if !recreate_member_list {
|
||||
added_id = Some(contact_id);
|
||||
}
|
||||
is_new_member = !chat_contacts.contains(&contact_id);
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.");
|
||||
is_new_member = false;
|
||||
}
|
||||
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
|
||||
if is_new_member || self_added {
|
||||
better_msg =
|
||||
Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
if allow_member_list_changes {
|
||||
if !recreate_member_list {
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
added_id = Some(contact_id);
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||
}
|
||||
better_msg.get_or_insert_with(Default::default);
|
||||
} else if let Some(old_name) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.map(|s| s.trim())
|
||||
@@ -2467,16 +2471,14 @@ async fn create_or_lookup_mailinglist(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn compute_mailinglist_name(
|
||||
list_id_header: &str,
|
||||
listid: &str,
|
||||
mime_parser: &MimeMessage,
|
||||
) -> String {
|
||||
let mut name = match LIST_ID_REGEX
|
||||
.captures(list_id_header)
|
||||
.and_then(|caps| caps.get(1))
|
||||
{
|
||||
Some(cap) => cap.as_str().trim().to_string(),
|
||||
let mut name = match LIST_ID_REGEX.captures(list_id_header) {
|
||||
Some(cap) => cap[1].trim().to_string(),
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
@@ -2523,11 +2525,8 @@ fn compute_mailinglist_name(
|
||||
// 51231231231231231231231232869f58.xing.com -> xing.com
|
||||
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
|
||||
if let Some(cap) = PREFIX_32_CHARS_HEX
|
||||
.captures(listid)
|
||||
.and_then(|caps| caps.get(2))
|
||||
{
|
||||
name = cap.as_str().to_string();
|
||||
if let Some(cap) = PREFIX_32_CHARS_HEX.captures(listid) {
|
||||
name = cap[2].to_string();
|
||||
} else {
|
||||
name = listid.to_string();
|
||||
}
|
||||
@@ -2686,7 +2685,7 @@ async fn update_verified_keys(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(peerstate) = &mut mimeparser.peerstate else {
|
||||
let Some(peerstate) = &mut mimeparser.decryption_info.peerstate else {
|
||||
// No peerstate means no verified keys.
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -2759,7 +2758,7 @@ async fn has_verified_encryption(
|
||||
// this check is skipped for SELF as there is no proper SELF-peerstate
|
||||
// and results in group-splits otherwise.
|
||||
if from_id != ContactId::SELF {
|
||||
let Some(peerstate) = &mimeparser.peerstate else {
|
||||
let Some(peerstate) = &mimeparser.decryption_info.peerstate else {
|
||||
return Ok(NotVerified(
|
||||
"No peerstate, the contact isn't verified".to_string(),
|
||||
));
|
||||
|
||||
+8
-109
@@ -883,54 +883,6 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_concat_multiple_ndns() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr("alice@posteo.org").await;
|
||||
let mid = "1234@mail.gmail.com";
|
||||
receive_imf(
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@posteo.org\n\
|
||||
To: hanerthaertidiuea@gmx.de\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <1234@mail.gmail.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: alice@example.org\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
let msg_id = chats.get_msg_id(0)?.unwrap();
|
||||
|
||||
let raw = include_str!("../../test-data/message/posteo_ndn.eml");
|
||||
let raw = raw.replace(
|
||||
"Message-ID: <04422840-f884-3e37-5778-8192fe22d8e1@posteo.de>",
|
||||
&format!("Message-ID: <{}>", mid),
|
||||
);
|
||||
receive_imf(&t, raw.as_bytes(), false).await?;
|
||||
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
|
||||
let err = "Undelivered Mail Returned to Sender – This is the mail system at host mout01.posteo.de.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hanerthaertidiuea@gmx.de>: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)".to_string();
|
||||
assert_eq!(msg.error(), Some(err.clone()));
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
|
||||
let raw = raw.replace(
|
||||
"Message-Id: <20200609184422.DCB6B1200DD@mout01.posteo.de>",
|
||||
"Message-Id: <next@mout01.posteo.de>",
|
||||
);
|
||||
receive_imf(&t, raw.as_bytes(), false).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
|
||||
assert_eq!(msg.error(), Some([err.clone(), err].join("\n\n")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
@@ -2226,7 +2178,8 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::BccSelf, false).await?;
|
||||
let chat_id = bob.get_self_chat().await.id;
|
||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = "Happy birthday to me".to_string();
|
||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
Ok(())
|
||||
@@ -3345,7 +3298,8 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
|
||||
assert_eq!(received_group.name, "Group");
|
||||
assert_eq!(received_group.can_send(&alice1).await?, false); // Can't send because it's Blocked::Request
|
||||
|
||||
let mut msg_out = Message::new_text("Private reply".to_string());
|
||||
let mut msg_out = Message::new(Viewtype::Text);
|
||||
msg_out.set_text("Private reply".to_string());
|
||||
|
||||
assert_eq!(received_group.blocked, Blocked::Request);
|
||||
msg_out.set_quote(&alice1, Some(&received)).await?;
|
||||
@@ -3552,7 +3506,8 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let received_group = Chat::load_from_db(&alice, received.chat_id).await?;
|
||||
assert_eq!(received_group.typ, Chattype::Group);
|
||||
|
||||
let mut msg_out = Message::new_text("Private reply".to_string());
|
||||
let mut msg_out = Message::new(Viewtype::Text);
|
||||
msg_out.set_text("Private reply".to_string());
|
||||
msg_out.set_quote(&alice, Some(&received)).await?;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
@@ -4185,8 +4140,9 @@ async fn test_recreate_contact_list_on_missing_message() -> Result<()> {
|
||||
// readd fiona
|
||||
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
|
||||
|
||||
alice.recv_msg(&remove_msg).await;
|
||||
|
||||
// delayed removal of fiona shouldn't remove her
|
||||
alice.recv_msg_trash(&remove_msg).await;
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
|
||||
|
||||
Ok(())
|
||||
@@ -4855,37 +4811,6 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protected_group_reply_from_mua() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(alice, fiona).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob, fiona])
|
||||
.await;
|
||||
let sent = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
bob_msg.chat_id.accept(bob).await?;
|
||||
// This is hacky, but i don't know other simple way to simulate a MUA reply. It works because
|
||||
// the message is correctly assigned to the chat by `References:`.
|
||||
bob.sql
|
||||
.execute(
|
||||
"UPDATE chats SET protected=0, grpid='' WHERE id=?",
|
||||
(bob_msg.chat_id,),
|
||||
)
|
||||
.await?;
|
||||
let sent = bob
|
||||
.send_text(bob_msg.chat_id, "/me replying from MUA")
|
||||
.await;
|
||||
let alice_msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(alice_msg.chat_id, alice_chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_older_message_from_2nd_device() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4977,32 +4902,6 @@ async fn test_unarchive_on_member_removal() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_op_member_added_is_trash() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "foos", &[bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&msg).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
|
||||
let fiona_id = Contact::create(bob, "", "fiona@example.net").await?;
|
||||
add_contact_to_chat(bob, bob_chat_id, fiona_id).await?;
|
||||
bob.recv_msg_trash(&msg).await;
|
||||
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forged_from() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
+7
-3
@@ -135,7 +135,7 @@ impl SchedulerState {
|
||||
/// If in the meantime [`SchedulerState::start`] or [`SchedulerState::stop`] is called
|
||||
/// resume will do the right thing and restore the scheduler to the state requested by
|
||||
/// the last call.
|
||||
pub(crate) async fn pause(&'_ self, context: Context) -> Result<IoPausedGuard> {
|
||||
pub(crate) async fn pause<'a>(&'_ self, context: Context) -> Result<IoPausedGuard> {
|
||||
{
|
||||
let mut inner = self.inner.write().await;
|
||||
match *inner {
|
||||
@@ -655,7 +655,9 @@ async fn fetch_idle(
|
||||
ctx,
|
||||
"IMAP session does not support IDLE, going to fake idle."
|
||||
);
|
||||
connection.fake_idle(ctx, watch_folder).await?;
|
||||
connection
|
||||
.fake_idle(ctx, &mut session, watch_folder, folder_meaning)
|
||||
.await?;
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
@@ -667,7 +669,9 @@ async fn fetch_idle(
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
|
||||
connection.fake_idle(ctx, watch_folder).await?;
|
||||
connection
|
||||
.fake_idle(ctx, &mut session, watch_folder, folder_meaning)
|
||||
.await?;
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
|
||||
@@ -318,6 +318,10 @@ impl Context {
|
||||
.yellow {
|
||||
background-color: #fdc625;
|
||||
}
|
||||
.not-started-error {
|
||||
font-size: 2em;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>"#
|
||||
@@ -337,10 +341,7 @@ impl Context {
|
||||
sched.smtp.state.connectivity.clone(),
|
||||
),
|
||||
_ => {
|
||||
ret += &format!(
|
||||
"<h3>{}</h3>\n</body></html>\n",
|
||||
stock_str::not_connected(self).await
|
||||
);
|
||||
ret += "<div class=\"not-started-error\">Error: IO Not Started</div><p>Please report this issue to the app developer.</p>\n</body></html>\n";
|
||||
return Ok(ret);
|
||||
}
|
||||
};
|
||||
|
||||
+20
-38
@@ -104,7 +104,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
|
||||
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
&group_name_urlencoded,
|
||||
@@ -119,7 +119,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"https://i.delta.chat/#{}&a={}&n={}&i={}&s={}",
|
||||
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
self_name_urlencoded,
|
||||
@@ -136,7 +136,7 @@ async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
|
||||
let key = load_self_public_key(context)
|
||||
.await
|
||||
.context("Failed to load key")?;
|
||||
Ok(key.dc_fingerprint())
|
||||
Ok(key.fingerprint())
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
|
||||
@@ -244,10 +244,10 @@ async fn verify_sender_by_fingerprint(
|
||||
|
||||
/// What to do with a Secure-Join handshake message after it was handled.
|
||||
///
|
||||
/// This status is returned to [`receive_imf_inner`] which will use it to decide what to do
|
||||
/// This status is returned to [`receive_imf`] which will use it to decide what to do
|
||||
/// next with this incoming setup-contact/secure-join handshake message.
|
||||
///
|
||||
/// [`receive_imf_inner`]: crate::receive_imf::receive_imf_inner
|
||||
/// [`receive_imf`]: crate::receive_imf::receive_imf
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum HandshakeMessage {
|
||||
/// The message has been fully handled and should be removed/delete.
|
||||
@@ -279,6 +279,7 @@ pub(crate) enum HandshakeMessage {
|
||||
///
|
||||
/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
|
||||
/// database; this is done by `receive_imf()` later on as needed.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) async fn handle_securejoin_handshake(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
@@ -297,9 +298,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
|
||||
if !matches!(step, "vg-request" | "vc-request") {
|
||||
let mut self_found = false;
|
||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||
let self_fingerprint = load_self_public_key(context).await?.fingerprint();
|
||||
for (addr, key) in &mime_message.gossiped_keys {
|
||||
if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
|
||||
if key.fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
|
||||
self_found = true;
|
||||
break;
|
||||
}
|
||||
@@ -347,7 +348,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
send_alice_handshake_msg(
|
||||
context,
|
||||
contact_id,
|
||||
&format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
|
||||
&format!("{}-auth-required", &step[..2]),
|
||||
)
|
||||
.await
|
||||
.context("failed sending auth-required handshake message")?;
|
||||
@@ -764,7 +765,6 @@ mod tests {
|
||||
WrongAliceGossip,
|
||||
SecurejoinWaitTimeout,
|
||||
AliceIsBot,
|
||||
AliceHasName,
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -792,21 +792,10 @@ mod tests {
|
||||
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_alice_has_name() {
|
||||
test_setup_contact_ex(SetupContactCase::AliceHasName).await
|
||||
}
|
||||
|
||||
async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
||||
if case == SetupContactCase::AliceHasName {
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let bob = tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await
|
||||
@@ -851,10 +840,7 @@ mod tests {
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
||||
@@ -935,10 +921,7 @@ mod tests {
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.dc_fingerprint();
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await.unwrap().fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
@@ -991,7 +974,6 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||
assert!(contact_bob.get_name().is_empty());
|
||||
assert_eq!(contact_bob.is_bot(), false);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
@@ -1021,13 +1003,14 @@ mod tests {
|
||||
}
|
||||
|
||||
// Make sure Alice hasn't yet sent their name to Bob.
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
match case {
|
||||
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
||||
_ => assert_eq!(contact_alice.get_authname(), ""),
|
||||
};
|
||||
assert_eq!(contact_alice.get_authname(), "");
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
@@ -1050,7 +1033,6 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
||||
assert!(contact_alice.get_name().is_empty());
|
||||
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
||||
|
||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||
@@ -1108,10 +1090,10 @@ mod tests {
|
||||
last_seen_autocrypt: 10,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(alice_pubkey.clone()),
|
||||
public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||
public_key_fingerprint: Some(alice_pubkey.fingerprint()),
|
||||
gossip_key: Some(alice_pubkey.clone()),
|
||||
gossip_timestamp: 10,
|
||||
gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
@@ -1159,7 +1141,7 @@ mod tests {
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
@@ -1322,7 +1304,7 @@ mod tests {
|
||||
"vg-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
|
||||
@@ -389,7 +389,7 @@ async fn send_handshake_message(
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
|
||||
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
|
||||
let bob_fp = load_self_public_key(context).await?.dc_fingerprint();
|
||||
let bob_fp = load_self_public_key(context).await?.fingerprint();
|
||||
msg.param.set(Param::Arg3, bob_fp.hex());
|
||||
|
||||
// Sends the grpid in the Secure-Join-Group header.
|
||||
|
||||
+21
-53
@@ -1,5 +1,4 @@
|
||||
//! # Simplify incoming plaintext.
|
||||
use crate::tools::IsNoneOrEmpty;
|
||||
|
||||
/// Protects lines starting with `--` against being treated as a footer.
|
||||
/// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||
@@ -21,20 +20,20 @@ pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
/// Returns `(lines, footer_lines)` tuple;
|
||||
/// `footer_lines` is set to `Some` if the footer was actually removed from `lines`
|
||||
/// (which is equal to the input array otherwise).
|
||||
pub(crate) fn remove_message_footer<'a>(
|
||||
lines: &'a [&str],
|
||||
) -> (&'a [&'a str], Option<&'a [&'a str]>) {
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<&'a [&'a str]>) {
|
||||
let mut nearly_standard_footer = None;
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
match line {
|
||||
// some providers encode `-- ` to `-- =20` which results in `-- `
|
||||
"-- " | "-- " => return (lines.get(..ix).unwrap_or(lines), lines.get(ix + 1..)),
|
||||
"-- " | "-- " => return (&lines[..ix], lines.get(ix + 1..)),
|
||||
// some providers encode `-- ` to `=2D-` which results in only `--`;
|
||||
// use that only when no other footer is found
|
||||
// and if the line before is empty and the line after is not empty
|
||||
"--" => {
|
||||
if (ix == 0 || lines.get(ix.saturating_sub(1)).is_none_or_empty())
|
||||
&& !lines.get(ix + 1).is_none_or_empty()
|
||||
if (ix == 0 || lines[ix - 1].is_empty())
|
||||
&& ix != lines.len() - 1
|
||||
&& !lines[ix + 1].is_empty()
|
||||
{
|
||||
nearly_standard_footer = Some(ix);
|
||||
}
|
||||
@@ -43,7 +42,7 @@ pub(crate) fn remove_message_footer<'a>(
|
||||
}
|
||||
}
|
||||
if let Some(ix) = nearly_standard_footer {
|
||||
return (lines.get(..ix).unwrap_or(lines), lines.get(ix + 1..));
|
||||
return (&lines[..ix], lines.get(ix + 1..));
|
||||
}
|
||||
(lines, None)
|
||||
}
|
||||
@@ -52,6 +51,7 @@ pub(crate) fn remove_message_footer<'a>(
|
||||
/// Returns `(lines, is_footer_removed)` tuple;
|
||||
/// `is_footer_removed` is set to `true` if the footer was actually removed from `lines`
|
||||
/// (which is equal to the input array otherwise).
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
if line == "--"
|
||||
@@ -61,10 +61,7 @@ fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
|| line.starts_with("*****")
|
||||
|| line.starts_with("~~~~~")
|
||||
{
|
||||
// `get` should always return `Some` here.
|
||||
if let Some(lines) = lines.get(..ix) {
|
||||
return (lines, true);
|
||||
}
|
||||
return (&lines[..ix], true);
|
||||
}
|
||||
}
|
||||
(lines, false)
|
||||
@@ -174,6 +171,7 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
|
||||
let mut first_quoted_line = lines.len();
|
||||
let mut last_quoted_line = None;
|
||||
@@ -188,36 +186,30 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
|
||||
}
|
||||
}
|
||||
if let Some(mut l_last) = last_quoted_line {
|
||||
let quoted_text = lines
|
||||
let quoted_text = lines[l_last..first_quoted_line]
|
||||
.iter()
|
||||
.take(first_quoted_line)
|
||||
.skip(l_last)
|
||||
.map(|s| {
|
||||
s.strip_prefix('>')
|
||||
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
if l_last > 1 {
|
||||
if let Some(line) = lines.get(l_last - 1) {
|
||||
if is_empty_line(line) {
|
||||
l_last -= 1
|
||||
}
|
||||
}
|
||||
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
|
||||
l_last -= 1
|
||||
}
|
||||
if l_last > 1 {
|
||||
if let Some(line) = lines.get(l_last - 1) {
|
||||
if is_quoted_headline(line) {
|
||||
l_last -= 1
|
||||
}
|
||||
let line = lines[l_last - 1];
|
||||
if is_quoted_headline(line) {
|
||||
l_last -= 1
|
||||
}
|
||||
}
|
||||
(lines.get(..l_last).unwrap_or(lines), Some(quoted_text))
|
||||
(&lines[..l_last], Some(quoted_text))
|
||||
} else {
|
||||
(lines, None)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_top_quote<'a>(
|
||||
lines: &'a [&str],
|
||||
is_chat_message: bool,
|
||||
@@ -244,12 +236,10 @@ fn remove_top_quote<'a>(
|
||||
}
|
||||
if let Some(last_quoted_line) = last_quoted_line {
|
||||
(
|
||||
lines.get(last_quoted_line + 1..).unwrap_or(lines),
|
||||
&lines[last_quoted_line + 1..],
|
||||
Some(
|
||||
lines
|
||||
lines[first_quoted_line..last_quoted_line + 1]
|
||||
.iter()
|
||||
.take(last_quoted_line + 1)
|
||||
.skip(first_quoted_line)
|
||||
.map(|s| {
|
||||
s.strip_prefix('>')
|
||||
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
|
||||
@@ -306,7 +296,7 @@ fn is_quoted_headline(buf: &str) -> bool {
|
||||
- Currently, we simply check if the last character is a ':'.
|
||||
- Checking for the existence of an email address may fail (headlines may show the user's name instead of the address) */
|
||||
|
||||
buf.len() <= 120 && buf.ends_with(':')
|
||||
buf.len() <= 80 && buf.ends_with(':')
|
||||
}
|
||||
|
||||
fn is_plain_quote(buf: &str) -> bool {
|
||||
@@ -411,28 +401,6 @@ mod tests {
|
||||
assert!(!is_plain_quote(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_quoted_headline() {
|
||||
assert!(is_quoted_headline("On 2024-08-28, Bob wrote:"));
|
||||
assert!(is_quoted_headline("Am 11. November 2024 schrieb Alice:"));
|
||||
assert!(is_quoted_headline("Anonymous Longer Name a écrit:"));
|
||||
assert!(is_quoted_headline("There is not really a pattern wrote:"));
|
||||
assert!(is_quoted_headline(
|
||||
"On Mon, 3 Jan, 2022 at 8:34 PM \"Anonymous Longer Name\" <anonymous-longer-name@example.com> wrote:"
|
||||
));
|
||||
assert!(!is_quoted_headline(
|
||||
"How are you? I just want to say that this line does not belong to the quote!"
|
||||
));
|
||||
assert!(!is_quoted_headline(
|
||||
"No quote headline as not ending with a colon"
|
||||
));
|
||||
assert!(!is_quoted_headline(
|
||||
"Even though this ends with a colon, \
|
||||
this is no quote-headline as just too long for most cases of date+name+address. \
|
||||
it's all heuristics only, it is expected to go wrong sometimes. there is always the 'Show full message' button:"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_top_quote() {
|
||||
let (lines, top_quote) = remove_top_quote(&["> first", "> second"], true);
|
||||
|
||||
+6
-13
@@ -126,7 +126,6 @@ impl Smtp {
|
||||
|
||||
let login_params =
|
||||
prioritize_server_login_params(&context.sql, login_params, "smtp").await?;
|
||||
let mut first_error = None;
|
||||
for lp in login_params {
|
||||
info!(context, "SMTP trying to connect to {}.", &lp.connection);
|
||||
let transport = match connect::connect_and_auth(
|
||||
@@ -144,7 +143,6 @@ impl Smtp {
|
||||
Ok(transport) => transport,
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP failed to connect and authenticate: {err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -159,7 +157,7 @@ impl Smtp {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(first_error.unwrap_or_else(|| format_err!("No SMTP connection candidates provided")))
|
||||
Err(format_err!("SMTP failed to connect"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,9 +355,9 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
.await
|
||||
.context("failed to update retries count")?;
|
||||
|
||||
let Some((body, recipients, msg_id, retries)) = context
|
||||
let (body, recipients, msg_id, retries) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
.query_row(
|
||||
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
|
||||
(rowid,),
|
||||
|row| {
|
||||
@@ -370,15 +368,10 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
Ok((mime, recipients, msg_id, retries))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
.await?;
|
||||
if retries > 6 {
|
||||
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.")
|
||||
.await?;
|
||||
}
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.").await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
|
||||
+22
-16
@@ -17,7 +17,7 @@ use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::location::delete_orphaned_poi_locations;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::net::dns::prune_dns_cache;
|
||||
use crate::net::prune_connection_history;
|
||||
use crate::param::{Param, Params};
|
||||
@@ -154,28 +154,29 @@ impl Sql {
|
||||
// don't have main database passphrase at this point.
|
||||
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
|
||||
// Without resetting import may fail due to existing tables.
|
||||
res.and_then(|_| {
|
||||
let res = res.and_then(|_| {
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
|
||||
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")
|
||||
})
|
||||
.and_then(|_| {
|
||||
});
|
||||
let res = res.and_then(|_| {
|
||||
conn.execute("VACUUM", [])
|
||||
.context("failed to vacuum the database")
|
||||
})
|
||||
.and(
|
||||
});
|
||||
let res = res.and(
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
|
||||
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE"),
|
||||
)
|
||||
.and_then(|_| {
|
||||
);
|
||||
let res = res.and_then(|_| {
|
||||
conn.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
|
||||
Ok(())
|
||||
})
|
||||
.context("failed to import from attached backup database")
|
||||
})
|
||||
.and(
|
||||
});
|
||||
let res = res.and(
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database"),
|
||||
)?;
|
||||
);
|
||||
res?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -246,7 +247,8 @@ impl Sql {
|
||||
// We now always watch all folders and delete messages there if delete_server is enabled.
|
||||
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
|
||||
if context.get_config_delete_server_after().await?.is_some() {
|
||||
let mut msg = Message::new_text(stock_str::delete_server_turned_off(context).await);
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(stock_str::delete_server_turned_off(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
context
|
||||
.set_config_internal(Config::DeleteServerAfter, Some("0"))
|
||||
@@ -942,12 +944,15 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
|
||||
let name_to_check = if let Some(namespc) = namespc_opt {
|
||||
let Some(name) = name.strip_suffix(namespc) else {
|
||||
let name_len = name.len();
|
||||
let namespc_len = namespc.len();
|
||||
if name_len <= namespc_len || !name.ends_with(namespc) {
|
||||
return false;
|
||||
};
|
||||
name
|
||||
}
|
||||
&name[..name_len - namespc_len]
|
||||
} else {
|
||||
name
|
||||
};
|
||||
@@ -1124,7 +1129,8 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
||||
let mut new_draft = Message::new_text("This is my draft".to_string());
|
||||
let mut new_draft = Message::new(Viewtype::Text);
|
||||
new_draft.set_text("This is my draft".to_string());
|
||||
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
|
||||
|
||||
housekeeping(&t).await.unwrap();
|
||||
|
||||
+2
-1
@@ -1419,7 +1419,8 @@ impl Context {
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
|
||||
|
||||
let mut msg = Message::new_text(welcome_message(self).await);
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = welcome_message(self).await;
|
||||
chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+6
-4
@@ -289,7 +289,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
use crate::param::Param;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils as test;
|
||||
|
||||
async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
|
||||
assert_eq!(msg.get_summary_text(ctx).await, expected);
|
||||
@@ -298,14 +298,15 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_summary_text() {
|
||||
let d = TestContext::new_alice().await;
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
|
||||
.await
|
||||
.unwrap();
|
||||
let some_text = " bla \t\n\tbla\n\t".to_string();
|
||||
|
||||
let msg = Message::new_text(some_text.to_string());
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(some_text.to_string());
|
||||
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
@@ -414,7 +415,8 @@ mod tests {
|
||||
}
|
||||
|
||||
// Forwarded
|
||||
let mut msg = Message::new_text(some_text.clone());
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.param.set_int(Param::Forwarded, 1);
|
||||
assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); // for simple text, the type is not added to the summary
|
||||
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); // skipping prefix used for reactions summaries
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user