Compare commits

..

50 Commits

Author SHA1 Message Date
Simon Laux
482abe154e make the repl tool work with the tokio console
https://github.com/tokio-rs/console/tree/main

this can help with debugging async stuff.
2023-05-22 21:02:30 +02:00
link2xt
c68a2e3820 ci: require Python 3.8 for deltachat-rpc-client
With Python 3.7 asynchronous tests randomly fail
with "RuntimeError: Event loop is closed" during shutdown.
Backtrace of the error includes `SafeChildWatcher` calls.

Python 3.8 has replaced `SafeChildWatcher`
with a new `ThreadedChildWatcher` by default [1]
as a bugfix for
"asyncio.create_subprocess_exec() only works with main event loop" bug [2].

Python 3.7 scheduled end of life is 2023-06-27
according to <https://devguide.python.org/versions/>.

[1] https://github.com/python/cpython/pull/14344
[2] https://bugs.python.org/issue35621
2023-05-21 13:08:03 +00:00
link2xt
c7ad0b1f4f test: make test_group_with_removed_message_id less flaky
It failed in CI once, apparently
because "Gr." was generated inside the group ID.
2023-05-21 11:50:41 +00:00
link2xt
0dd9e3a77e docs: add contributing guidelines
Co-authored-by: Hocuri <hocuri@gmx.de>
2023-05-20 10:34:02 +00:00
link2xt
d27e3d085e ci: remove incorrect comment 2023-05-19 11:03:26 +00:00
link2xt
10b2aa5350 chore: remove release.toml
We are not using cargo-release at the moment.
2023-05-19 09:25:27 +00:00
link2xt
3a29a555bf test(python): make test_mdn_asymmetric less flaky
Normally the message has UID 1,
but this is not true when account is reused.
In this case the message may be
"Marked messages 2 in folder DeltaChat as seen."
and the test times out waiting for
"Marked messages 1 in folder DeltaChat as seen."
2023-05-18 18:39:40 +00:00
link2xt
f024f396bf docs: document release process in RELEASE.md 2023-05-17 10:53:26 +00:00
link2xt
24d52c5909 build(set_core_version.py): expect release date in the changelog
git-cliff adds the date to the changelog automatically,
so set_core_version.py only needs to check that the date is in place.
2023-05-16 22:47:28 +00:00
link2xt
f9dc8edbcb feat: add more context to message loading errors 2023-05-16 19:40:38 +00:00
link2xt
2e6f98f4e4 api: add dc_jsonrpc_blocking_call() 2023-05-15 19:55:13 +00:00
link2xt
50431d8cfe build(git-cliff): add "Documentation" section 2023-05-15 15:34:16 +00:00
link2xt
55fcd589db build(git-cliff): put "ci" commits into "CI" section of changelog 2023-05-15 15:34:16 +00:00
link2xt
081178d623 ci(mergeable): allow PR titles to start with "ci" and "build" 2023-05-15 15:34:16 +00:00
link2xt
92d5857150 build(git-cliff): update the link to configuration documentation 2023-05-15 15:34:16 +00:00
link2xt
bb45c249a3 build(git-cliff): add unconventional commits to "Other" section 2023-05-15 15:34:16 +00:00
link2xt
8796e0472a build(git-cliff): add scope to changelog entries 2023-05-15 15:34:16 +00:00
link2xt
3bd16ba045 build(git-cliff): add "Build system" section 2023-05-15 15:34:16 +00:00
link2xt
4b7ff6f003 build(git-cliff): add period at the end of changelog entry titles
Make it consistent with the rest of the changelog.
2023-05-15 15:34:16 +00:00
link2xt
53449ea5b3 build(git-cliff): remove git-cliff footer 2023-05-15 15:34:16 +00:00
link2xt
3b381c4862 docs: update instructions for python devenv 2023-05-15 15:33:26 +00:00
link2xt
ce729263a5 chore(release): prepare for v1.115.0 2023-05-13 18:45:51 +00:00
link2xt
67480999c0 feat: do not cut incoming text if "bot" config is set 2023-05-13 15:53:48 +00:00
Simon Laux
fb8b9f60ce api: jsonrpc: new MessageReadReceipt type and get_message_read_receipts(account_id, message_id) jsonrpc method 2023-05-12 16:32:52 +02:00
Simon Laux
9ed36d4e05 api: rust: add get_msg_read_receipts(context, msg_id) - get the contacts that send read receipts for a message 2023-05-12 16:32:52 +02:00
link2xt
e3c01d76c4 fix: do not return an error from send_msg_to_smtp if retry limit is exceeded
This prevents sending the other messages for which the limit was not exceeded.
2023-05-12 13:26:37 +00:00
link2xt
cb7f96449d Merge tag 'v1.112.9' into 'master' 2023-05-12 11:35:30 +00:00
link2xt
e4f4dacaf0 chore(release): prepare for 1.112.9 2023-05-12 11:17:54 +00:00
link2xt
9fc1fe74ad fix: delete smtp rows when message sending is cancelled 2023-05-12 11:17:23 +00:00
link2xt
991089d98e feat: improve SMTP logging 2023-05-12 10:07:55 +00:00
link2xt
c7a250da31 fix: fetch at most 100 existing messages even if EXISTS was not received
According to RFC 3501, EXISTS must always be sent in response to SELECT.
But if the server does not send it for some reason,
async-imap uses the default value, so we will essentially fetch `1:*`
and downloading all messages may take a long time.
2023-05-12 10:07:29 +00:00
link2xt
c5b6bad956 fix: recreate smtp table with AUTOINCREMENT
This ensures the rows cannot be confused accidentally
when referred by row_id.
2023-05-12 09:53:58 +00:00
link2xt
c4534ff621 feat: improve SMTP logging 2023-05-12 09:53:32 +00:00
Hocuri
b2c299fa82 fix: Don't put a double dot at the end of error messages (#4398) 2023-05-11 18:43:22 +02:00
link2xt
68aa15950a fix: fix PR URL template in git-cliff configuration (#4396) 2023-05-11 17:43:52 +02:00
Asiel Díaz Benítez
4a593a8d7e api!:jsonrpc: sort reactions in descending order (#4388)
convert `JSONRPCReactions.reactions` to a `Vec<JSONRPCReaction>` with unique reactions and their count, sorted in descending order.
---------

Co-authored-by: link2xt <link2xt@testrun.org>
2023-05-11 08:29:43 -04:00
Hocuri
2328ba54be feat: Generate our changelogs using git-cliff (#4393) 2023-05-10 18:18:42 +02:00
link2xt
e216dfd655 fix: delete smtp rows when message sending is cancelled 2023-05-10 16:08:00 +00:00
link2xt
86472aba2c refactor: iterate over msg_ids without .iter() 2023-05-10 14:32:38 +00:00
link2xt
15ce54edfb Update set_core_version.py with new release process 2023-05-10 12:00:14 +00:00
Sebastian Klähn
b7bbb3ee9a Allow sending a raw webxdc status update (#4386)
- create new api endpoint

Co-authored-by: bjoern <r10s@b44t.com>

* renaming function

---------

Co-authored-by: septias <xxsebastian.kleahnxx@gmail.com>
Co-authored-by: bjoern <r10s@b44t.com>
2023-05-09 21:15:09 +02:00
link2xt
d3236e79fd fix: fetch at most 100 existing messages even if EXISTS was not received
According to RFC 3501, EXISTS must always be sent in response to SELECT.
But if the server does not send it for some reason,
async-imap uses the default value, so we will essentially fetch `1:*`
and downloading all messages may take a long time.
2023-05-09 10:56:01 +00:00
Simon Laux
aa212b2b7e jsonrpc: MessageSearchResult, always include chat_name(not an option anymore), also add author_id and chat_type (#4374)
* jsonrpc: `MessageSearchResult`, always include `chat_name`(not an option anymore), also add `author_id` and `chat_type`

* replace .ok_or_else with anyhow context
2023-05-09 10:22:32 +00:00
Simon Laux
a0c51b3c3a fix: jsonrpc: typescript client: fix types of events in event emitter (#4373)
* fix: jsonrpc: typescript client: fix types of events in event emitter

* add ts ignore to remove ts error (when building in ts5)

doing the types right via casting is to complicated IMO and has no real benefit here.
2023-05-09 09:57:49 +00:00
link2xt
e50d7724e3 feat: build deltachat-rpc-server releases for x86_64 macOS 2023-05-08 19:04:38 +00:00
meganoahj
0d30e66dda add thumbnail for image in chatlist item (#4311)
* add thumbnail for image in chatlist item

* added preview for stickers and gifs also

* added last_message_type and last_message_id to ChatListItemFetchResult

addresses link2ext comment

* code optimization

fixes:
https://github.com/deltachat/deltachat-core-rust/pull/4311#discussion_r1162791672

* code formatting

* fix clippy

* add changelog entry

---------

Co-authored-by: meganoahj <s3ccg@testrun.org>
Co-authored-by: Simon Laux <mobile.info@simonlaux.de>
2023-05-08 19:44:43 +02:00
Asiel Díaz Benítez
5c3df7e452 jsonrpc: use camel-case for Event struct and move events.rs to types folder (#4372)
* use camel-case for Event struct and move events.rs to types folder

---------

Co-authored-by: link2xt <link2xt@testrun.org>
2023-05-08 06:49:19 -04:00
dependabot[bot]
3efbe5b1ef cargo: bump regex from 1.7.3 to 1.8.1 (#4361) 2023-05-07 13:31:29 +00:00
dependabot[bot]
52cfba06ea Merge pull request #4356 from deltachat/dependabot/cargo/dirs-5.0.1 2023-05-06 17:58:24 +00:00
dependabot[bot]
afba63603e cargo: bump dirs from 5.0.0 to 5.0.1
Bumps [dirs](https://github.com/soc/dirs-rs) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/soc/dirs-rs/releases)
- [Commits](https://github.com/soc/dirs-rs/commits)

---
updated-dependencies:
- dependency-name: dirs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-06 11:36:44 +00:00
54 changed files with 973 additions and 409 deletions

View File

@@ -9,3 +9,6 @@
# invoking `cargo test` threads are allowed to have a large enough
# stack size without needing to use an optimised build.
RUST_MIN_STACK = "8388608"
[build]
rustflags = ["--cfg", "tokio_unstable"]

View File

@@ -5,11 +5,11 @@ mergeable:
validate:
- do: title
begins_with:
match: ['feat', 'fix', 'api', 'refactor', 'perf', 'test', 'style', 'chore']
match: ['feat', 'fix', 'api', 'refactor', 'perf', 'test', 'style', 'chore', 'cargo', 'build', 'ci', 'docs']
fail:
- do: checks
status: "action_required"
payload:
title: PR title should follow conventional commits
summary: "PR title should follow https://conventionalcommits.org. E.g. start with 'feat:' (for Features / Changes), 'fix:' (for Fixes), 'api:' (for API-Changes), 'api!:' (for breaking API-Changes) 'refactor:' (for Refactor), 'perf:' (for Performance), 'test:' (for Tests), 'style:' (for Styling), 'chore:' (for Miscellaneous Tasks)"
summary: "PR title should follow https://conventionalcommits.org. See https://github.com/deltachat/deltachat-core-rust/blob/master/CONTRIBUTING.md for details."

View File

@@ -222,7 +222,6 @@ jobs:
fail-fast: false
matrix:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.11
- os: macos-latest
@@ -234,11 +233,12 @@ jobs:
- os: macos-latest
python: pypy3.9
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built. Test it with minimum supported Rust version.
# Minimum Supported Python Version = 3.8
#
# Python 3.7 has at least one known bug related to starting subprocesses
# in asyncio programs: <https://bugs.python.org/issue35621>
- os: ubuntu-latest
python: 3.7
python: 3.8
runs-on: ${{ matrix.os }}
steps:

View File

@@ -84,9 +84,28 @@ jobs:
path: target/${{ matrix.target}}/release/${{ matrix.path }}
if-no-files-found: error
build_macos:
name: Build deltachat-rpc-server for macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup rust target
run: rustup target add x86_64-apple-darwin
- name: Build
run: cargo build --release --package deltachat-rpc-server --target x86_64-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-x86_64-macos
path: target/x86_64-apple-darwin/release/deltachat-rpc-server
if-no-files-found: error
publish:
name: Upload binaries to the release
needs: ["build_linux", "build_windows"]
needs: ["build_linux", "build_windows", "build_macos"]
permissions:
contents: write
runs-on: "ubuntu-latest"
@@ -97,7 +116,7 @@ jobs:
- name: Compose dist/ directory
run: |
mkdir dist
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe; do
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe x86_64-macos; do
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
done

View File

@@ -1,15 +1,58 @@
# Changelog
## Unreleased
## [1.115.0] - 2023-05-12
### JSON-RPC API Changes
- Sort reactions in descending order ([#4388](https://github.com/deltachat/deltachat-core-rust/pull/4388)).
- Add API to get reactions outside the message snapshot.
- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`.
- `get_chatlist_entries` now returns `Vec<u32>` of chatids instead of `ChatListEntries`.
- `JSONRPCReactions.reactions` is now a `Vec<JSONRPCReaction>` with unique reactions and their count, sorted in descending order.
- `Event`: `context_id` property is now called `contextId`.
- Expand `MessageSearchResult`:
- Always include `chat_name`(not an option anymore).
- Add `author_id`, `chat_type`, `chat_color`, `is_chat_protected`, `is_chat_contact_request`, `is_chat_archived`.
- `author_name` now contains the overridden sender name.
- `ChatListItemFetchResult` gets new properties: `summary_preview_image`, `last_message_type` and `last_message_id`
- New `MessageReadReceipt` type and `get_message_read_receipts(account_id, message_id)` jsonrpc method.
### API Changes
- New rust API `send_webxdc_status_update_struct` to send a `StatusUpdateItem`.
- Add `get_msg_read_receipts(context, msg_id)` - get the contacts that send read receipts for a message.
### Features / Changes
- Build deltachat-rpc-server releases for x86\_64 macOS.
- Generate changelogs using git-cliff ([#4393](https://github.com/deltachat/deltachat-core-rust/pull/4393), [#4396](https://github.com/deltachat/deltachat-core-rust/pull/4396)).
- Improve SMTP logging.
- Do not cut incoming text if "bot" config is set.
### Fixes
- JSON-RPC: typescript client: fix types of events in event emitter ([#4373](https://github.com/deltachat/deltachat-core-rust/pull/4373)).
- Fetch at most 100 existing messages even if EXISTS was not received ([#4383](https://github.com/deltachat/deltachat-core-rust/pull/4383)).
- Don't put a double dot at the end of error messages ([#4398](https://github.com/deltachat/deltachat-core-rust/pull/4398)).
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/deltachat/deltachat-core-rust/pull/4390)).
- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/deltachat/deltachat-core-rust/pull/4377)).
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/deltachat/deltachat-core-rust/pull/4391)).
### Refactor
- Iterate over `msg_ids` without .iter().
## [1.112.9] - 2023-05-12
### Fixes
- Fetch at most 100 existing messages even if EXISTS was not received.
- Delete `smtp` rows when message sending is cancelled.
### Changes
- BREAKING: jsonrpc:
- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`
- `get_chatlist_entries` now returns `Vec<u32>` of chatids instead of `ChatListEntries`
- JSON-RPC: add API to get reactions outside the message snapshot
### Fixes
- Make the bots automatically accept group chat contact requests. #4377
- Improve SMTP logging.
## [1.114.0] - 2023-04-24
@@ -2442,5 +2485,7 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.113.0
[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0

70
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,70 @@
# Contributing guidelines
## Reporting bugs
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
If the bug you found is specific to
[Android](https://github.com/deltachat/deltachat-android/issues),
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
report it to the corresponding repository.
## Proposing features
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
## Contributing code
If you want to contribute a code, [open a pull request](https://github.com/deltachat/deltachat-core-rust/pulls).
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].
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".
#### 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/
## Other ways to contribute
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).

179
Cargo.lock generated
View File

@@ -60,9 +60,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "0.7.20"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
dependencies = [
"memchr",
]
@@ -768,6 +768,42 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console-api"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e"
dependencies = [
"prost",
"prost-types",
"tonic",
"tracing-core",
]
[[package]]
name = "console-subscriber"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57ab2224a0311582eb03adba4caaf18644f7b1f10a760803a803b9b605187fc7"
dependencies = [
"console-api",
"crossbeam-channel",
"crossbeam-utils",
"futures",
"hdrhistogram",
"humantime 2.1.0",
"prost-types",
"serde",
"serde_json",
"thread_local",
"tokio",
"tokio-stream",
"tonic",
"tracing",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "const-oid"
version = "0.9.2"
@@ -1144,7 +1180,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.114.0"
version = "1.115.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1219,7 +1255,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.114.0"
version = "1.115.0"
dependencies = [
"anyhow",
"async-channel",
@@ -1242,10 +1278,11 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.114.0"
version = "1.115.0"
dependencies = [
"ansi_term",
"anyhow",
"console-subscriber",
"deltachat",
"dirs",
"log",
@@ -1257,7 +1294,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.114.0"
version = "1.115.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1282,7 +1319,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.114.0"
version = "1.115.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1295,6 +1332,7 @@ dependencies = [
"serde_json",
"thiserror",
"tokio",
"yerpc",
]
[[package]]
@@ -1422,9 +1460,9 @@ dependencies = [
[[package]]
name = "dirs"
version = "5.0.0"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
@@ -1441,13 +1479,14 @@ dependencies = [
[[package]]
name = "dirs-sys"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -2206,6 +2245,19 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "hdrhistogram"
version = "7.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8"
dependencies = [
"base64 0.13.1",
"byteorder",
"flate2",
"nom",
"num-traits",
]
[[package]]
name = "heck"
version = "0.4.1"
@@ -2367,6 +2419,18 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-timeout"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
dependencies = [
"hyper",
"pin-project-lite",
"tokio",
"tokio-io-timeout",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
@@ -3153,6 +3217,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_info"
version = "3.6.0"
@@ -3603,10 +3673,42 @@ dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_xorshift",
"regex-syntax",
"regex-syntax 0.6.29",
"unarray",
]
[[package]]
name = "prost"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "prost-types"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
dependencies = [
"prost",
]
[[package]]
name = "qrcodegen"
version = "1.8.0"
@@ -3874,13 +3976,13 @@ dependencies = [
[[package]]
name = "regex"
version = "1.7.3"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.7.1",
]
[[package]]
@@ -3889,7 +3991,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
"regex-syntax 0.6.29",
]
[[package]]
@@ -3898,6 +4000,12 @@ version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]]
name = "reqwest"
version = "0.11.17"
@@ -4844,6 +4952,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.48.0",
]
@@ -4979,6 +5088,34 @@ dependencies = [
"winnow",
]
[[package]]
name = "tonic"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a"
dependencies = [
"async-trait",
"axum",
"base64 0.21.0",
"bytes",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"hyper-timeout",
"percent-encoding",
"pin-project",
"prost",
"tokio",
"tokio-stream",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower"
version = "0.4.13"
@@ -4987,9 +5124,13 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"indexmap",
"pin-project",
"pin-project-lite",
"rand 0.8.5",
"slab",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
@@ -5741,9 +5882,9 @@ dependencies = [
[[package]]
name = "yerpc"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6a0257f42e6bdc187f37074723b6094da3502cee21ae517b3c54d2c37d506e7"
checksum = "b2c26a804eaa30c1ff1a296dc6dd1a7d7c622750dafcd0d6b2ed5e3c5c3beb22"
dependencies = [
"anyhow",
"async-channel",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.114.0"
version = "1.115.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"
@@ -69,7 +69,7 @@ pretty_env_logger = { version = "0.4", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.28"
rand = "0.8"
regex = "1.7"
regex = "1.8"
reqwest = { version = "0.11.17", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rust-hsluv = "0.1"
@@ -84,7 +84,7 @@ strum_macros = "0.24"
tagger = "4.3.4"
textwrap = "0.16.0"
thiserror = "1"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros", "tracing"] }
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar

18
RELEASE.md Normal file
View File

@@ -0,0 +1,18 @@
# Releasing a new version of DeltaChat core
For example, to release version 1.116.0 of the core, do the following steps.
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
3. Update the version by running `scripts/set_core_version.py 1.116.0`.
4. Commit the changes as `chore(release): prepare for 1.116.0`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
5. Tag the release: `git tag -a v1.116.0`.
6. Push the release tag: `git push origin v1.116.0`.
7. Create a GitHub release: `gh release create v1.116.0 -n ''`.

View File

@@ -1,17 +1,17 @@
# configuration file for git-cliff
# see https://github.com/orhun/git-cliff#configuration-file
# see https://git-cliff.org/docs/configuration/
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
filter_unconventional = false
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/PR/${2}))"}, # replace pull request / issue numbers
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
@@ -24,6 +24,10 @@ commit_parsers = [
{ message = "^style", group = "Styling"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ message = "^build", group = "Build system"},
{ message = "^docs", group = "Documentation"},
{ message = "^ci", group = "CI"},
{ message = ".*", group = "Other"},
# { body = ".*security", group = "Security"},
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
@@ -60,7 +64,9 @@ body = """
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
- {% if commit.breaking %}[**breaking**] {% endif %}\
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
{{ commit.message | upper_first }}.\
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
{% raw %} {% endraw %}- {{ footer.value }}\
{% endif %}{% endfor %}\
@@ -69,7 +75,3 @@ body = """
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.114.0"
version = "1.115.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -25,6 +25,7 @@ anyhow = "1"
thiserror = "1"
rand = "0.8"
once_cell = "1.17.0"
yerpc = { version = "0.4.4", features = ["anyhow_expose"] }
[features]
default = ["vendored"]

View File

@@ -461,8 +461,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
* adds Auto-Submitted header to outgoing messages,
* accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
* and does not cut large incoming text messages.
* - `last_msg_id` = database ID of the last message processed by the bot.
* This ID and IDs below it are guaranteed not to be returned
* by dc_get_next_msgs() and dc_wait_next_msgs().
@@ -5719,6 +5720,18 @@ void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* req
*/
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Make a JSON-RPC call and return a response.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param method JSON-RPC method name, e.g. `check_email_validity`.
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* On error, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
/**
* @class dc_event_emitter_t
*

View File

@@ -4967,7 +4967,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
use super::*;
@@ -5039,4 +5039,29 @@ mod jsonrpc {
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
method: *const libc::c_char,
params: *const libc::c_char,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
let method = to_string_lossy(method);
let params = to_string_lossy(params);
let params: Option<yerpc::Params> = match serde_json::from_str(&params) {
Ok(params) => Some(params),
Err(_) => None,
};
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
let res = block_on(api.handle.server().handle_request(method, params));
match res {
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.114.0"
version = "1.115.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -21,7 +21,7 @@ log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.96"
yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
yerpc = { version = "0.4.4", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.28.0" }
sanitize-filename = "0.4"

View File

@@ -4,6 +4,7 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::message::get_msg_read_receipts;
use deltachat::qr::Qr;
use deltachat::{
chat::{
@@ -35,21 +36,19 @@ use tokio::sync::{watch, Mutex, RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
pub mod events;
pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::contact::ContactObject;
use types::events::Event;
use types::http::HttpResponse;
use types::message::MessageData;
use types::message::MessageObject;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::provider_info::ProviderInfo;
use types::reactions::JSONRPCReactions;
use types::webxdc::WebxdcMessageInfo;
use self::events::Event;
use self::types::message::MessageLoadResult;
use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
@@ -1119,6 +1118,24 @@ impl CommandApi {
get_msg_info(&ctx, MsgId::new(message_id)).await
}
/// Returns contacts that sent read receipts and the time of reading.
async fn get_message_read_receipts(
&self,
account_id: u32,
message_id: u32,
) -> Result<Vec<MessageReadReceipt>> {
let ctx = self.get_context(account_id).await?;
let receipts = get_msg_read_receipts(&ctx, MsgId::new(message_id))
.await?
.iter()
.map(|(contact_id, ts)| MessageReadReceipt {
contact_id: contact_id.to_u32(),
timestamp: *ts,
})
.collect();
Ok(receipts)
}
/// Asks the core to start downloading a message fully.
/// This function is typically called when the user hits the "Download" button
/// that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`

View File

@@ -1,6 +1,6 @@
use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Context as _, Result};
use anyhow::{bail, Context as _, Result};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
@@ -92,10 +92,7 @@ impl FullChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
@@ -158,10 +155,7 @@ impl BasicChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
color,

View File

@@ -12,6 +12,7 @@ use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::message::MessageViewtype;
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
@@ -26,6 +27,8 @@ pub enum ChatListItemFetchResult {
summary_text1: String,
summary_text2: String,
summary_status: u32,
/// showing preview if last chat message is image
summary_preview_image: Option<String>,
is_protected: bool,
is_group: bool,
fresh_message_counter: usize,
@@ -42,6 +45,8 @@ pub enum ChatListItemFetchResult {
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
was_seen_recently: bool,
last_message_type: Option<MessageViewtype>,
last_message_id: Option<u32>,
},
#[serde(rename_all = "camelCase")]
ArchiveLink { fresh_message_counter: usize },
@@ -72,6 +77,8 @@ pub(crate) async fn get_chat_list_item_by_id(
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
let summary_text2 = summary.text.to_owned();
let summary_preview_image = summary.thumbnail_path;
let visibility = chat.get_visibility();
let avatar_path = chat
@@ -79,12 +86,15 @@ pub(crate) async fn get_chat_list_item_by_id(
.await?
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
let last_updated = match last_msgid {
let (last_updated, message_type) = match last_msgid {
Some(id) => {
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
Some(last_message.get_timestamp() * 1000)
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
}
None => None,
None => (None, None),
};
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
@@ -119,6 +129,7 @@ pub(crate) async fn get_chat_list_item_by_id(
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_protected: chat.is_protected(),
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
@@ -133,5 +144,7 @@ pub(crate) async fn get_chat_list_item_by_id(
is_broadcast: chat.get_type() == Chattype::Broadcast,
dm_chat_contact,
was_seen_recently,
last_message_type: message_type,
last_message_id: last_msgid.map(|id| id.to_u32()),
})
}

View File

@@ -3,6 +3,7 @@ use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct Event {
/// Event payload.
event: EventType,

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use anyhow::{Context as _, Result};
use deltachat::chat::Chat;
use deltachat::chat::ChatItem;
use deltachat::constants::Chattype;
use deltachat::chat::ChatVisibility;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download;
@@ -114,8 +114,12 @@ impl MessageObject {
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::load_from_db(context, message.get_from_id()).await?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
let sender_contact = Contact::load_from_db(context, message.get_from_id())
.await
.context("failed to load sender contact")?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
.await
.context("failed to load sender contact object")?;
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
let override_sender_name = message.get_override_sender_name();
@@ -132,7 +136,9 @@ impl MessageObject {
let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? {
Some(quote) => {
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
let quote_author = Contact::load_from_db(context, quote.get_from_id())
.await
.context("failed to load quote author contact")?;
Some(MessageQuote::WithMessage {
text: quoted_text,
message_id: quote.get_id().to_u32(),
@@ -160,7 +166,9 @@ impl MessageObject {
None
};
let reactions = get_msg_reactions(context, msg_id).await?;
let reactions = get_msg_reactions(context, msg_id)
.await
.context("failed to load message reactions")?;
let reactions = if reactions.is_empty() {
None
} else {
@@ -180,7 +188,7 @@ impl MessageObject {
state: message
.get_state()
.to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
.context("state conversion to number failed")?,
error: message.error(),
timestamp: message.get_timestamp(),
@@ -203,7 +211,7 @@ impl MessageObject {
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
.context("videochat type conversion to number failed")?,
),
None => None,
},
@@ -443,9 +451,17 @@ impl MessageNotificationInfo {
pub struct MessageSearchResult {
id: u32,
author_profile_image: Option<String>,
/// if sender name if overridden it will show it as ~alias
author_name: String,
author_color: String,
chat_name: Option<String>,
author_id: u32,
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
chat_type: u32,
is_chat_protected: bool,
is_chat_contact_request: bool,
is_chat_archived: bool,
message: String,
timestamp: i64,
}
@@ -460,17 +476,31 @@ impl MessageSearchResult {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let chat_profile_image = match chat.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let author_name = if let Some(name) = message.get_override_sender_name() {
format!("~{name}")
} else {
sender.get_display_name().to_owned()
};
let chat_color = color_int_to_hex_string(chat.get_color(context).await?);
Ok(Self {
id: msg_id.to_u32(),
author_profile_image: profile_image,
author_name: sender.get_display_name().to_owned(),
author_name,
author_color: color_int_to_hex_string(sender.get_color()),
chat_name: if chat.get_type() == Chattype::Single {
Some(chat.get_name().to_owned())
} else {
None
},
author_id: sender.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_profile_image,
is_chat_protected: chat.is_protected(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
message: message.get_text().unwrap_or_default(),
timestamp: message.get_timestamp(),
})
@@ -514,3 +544,10 @@ pub struct MessageData {
pub override_sender_name: Option<String>,
pub quoted_message_id: Option<u32>,
}
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageReadReceipt {
pub contact_id: u32,
pub timestamp: i64,
}

View File

@@ -2,6 +2,7 @@ pub mod account;
pub mod chat;
pub mod chat_list;
pub mod contact;
pub mod events;
pub mod http;
pub mod location;
pub mod message;

View File

@@ -1,23 +1,37 @@
use std::collections::BTreeMap;
use deltachat::contact::ContactId;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
/// A single reaction emoji.
#[derive(Serialize, TypeDef)]
#[serde(rename = "Reaction", rename_all = "camelCase")]
pub struct JSONRPCReaction {
/// Emoji.
emoji: String,
/// Emoji frequency.
count: usize,
/// True if we reacted with this emoji.
is_from_self: bool,
}
/// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count
reactions: BTreeMap<String, u32>,
/// Unique reactions and their count, sorted in descending order.
reactions: Vec<JSONRPCReaction>,
}
impl From<Reactions> for JSONRPCReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id);
@@ -30,18 +44,29 @@ impl From<Reactions> for JSONRPCReactions {
.map(|emoji| emoji.to_owned())
.collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
for emoji in emojis {
if let Some(x) = unique_reactions.get_mut(&emoji) {
*x += 1;
} else {
unique_reactions.insert(emoji, 1);
}
}
}
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
let mut reactions_v = Vec::new();
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
let is_from_self = if let Some(self_reactions) = self_reactions {
self_reactions.contains(&emoji)
} else {
false
};
let reaction = JSONRPCReaction {
emoji,
count,
is_from_self,
};
reactions_v.push(reaction)
}
JSONRPCReactions {
reactions_by_contact,
reactions: unique_reactions,
reactions: reactions_v,
}
}
}

View File

@@ -1,5 +1,4 @@
pub mod api;
pub use api::events;
pub use yerpc;
#[cfg(test)]

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.114.0"
"version": "1.115.0"
}

View File

@@ -1,33 +1,28 @@
import * as T from "../generated/types.js";
import { EventType } from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type DCWireEvent<T extends Event> = {
event: T;
contextId: number;
};
// export type Events = Record<
// Event["type"] | "ALL",
// (event: DeltaChatEvent<Event>) => void
// >;
type Events = { ALL: (accountId: number, event: Event) => void } & {
[Property in Event["type"]]: (
type Events = { ALL: (accountId: number, event: EventType) => void } & {
[Property in EventType["type"]]: (
accountId: number,
event: Extract<Event, { type: Property }>
event: Extract<EventType, { type: Property }>
) => void;
};
type ContextEvents = { ALL: (event: Event) => void } & {
[Property in Event["type"]]: (
event: Extract<Event, { type: Property }>
type ContextEvents = { ALL: (event: EventType) => void } & {
[Property in EventType["type"]]: (
event: Extract<EventType, { type: Property }>
) => void;
};
export type DcEvent = Event;
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
export type DcEvent = EventType;
export type DcEventType<T extends EventType["type"]> = Extract<
EventType,
{ type: T }
>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
@@ -50,16 +45,17 @@ export class BaseDeltaChat<
async eventLoop(): Promise<void> {
while (true) {
const event = await this.rpc.getNextEvent();
this.emit(event.event.type, event.context_id, event.event as any);
this.emit("ALL", event.context_id, event.event as any);
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event);
this.emit("ALL", event.contextId, event.event);
if (this.contextEmitters[event.context_id]) {
this.contextEmitters[event.context_id].emit(
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
//@ts-ignore
event.event as any
);
this.contextEmitters[event.context_id].emit("ALL", event.event as any);
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
}
}
}

View File

@@ -1,12 +1,13 @@
[package]
name = "deltachat-repl"
version = "1.114.0"
version = "1.115.0"
license = "MPL-2.0"
edition = "2021"
[dependencies]
ansi_term = "0.12.1"
anyhow = "1"
console-subscriber = "0.1.9"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.16"

View File

@@ -315,11 +315,13 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
let events = context.get_event_emitter();
tokio::task::spawn(async move {
while let Some(event) = events.recv().await {
receive_event(event.typ);
}
});
tokio::task::Builder::new()
.name("repl:receive_event")
.spawn(async move {
while let Some(event) = events.recv().await {
receive_event(event.typ);
}
})?;
println!("Delta Chat Core is awaiting your commands.");
@@ -331,61 +333,63 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
let mut selected_chat = ChatId::default();
let ctx = context.clone();
let input_loop = tokio::task::spawn_blocking(move || {
let h = DcHelper {
completer: FilenameCompleter::new(),
highlighter: MatchingBracketHighlighter::new(),
hinter: HistoryHinter {},
};
let mut rl = Editor::with_config(config)?;
rl.set_helper(Some(h));
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
if rl.load_history(".dc-history.txt").is_err() {
println!("No previous history.");
}
let input_loop = tokio::task::Builder::new()
.name("repl:input_loop")
.spawn_blocking(move || {
let h = DcHelper {
completer: FilenameCompleter::new(),
highlighter: MatchingBracketHighlighter::new(),
hinter: HistoryHinter {},
};
let mut rl = Editor::with_config(config)?;
rl.set_helper(Some(h));
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
if rl.load_history(".dc-history.txt").is_err() {
println!("No previous history.");
}
loop {
let p = "> ";
let readline = rl.readline(p);
loop {
let p = "> ";
let readline = rl.readline(p);
match readline {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str())?;
let should_continue = Handle::current().block_on(async {
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => true,
Ok(ExitResult::Exit) => {
println!("Exiting ...");
false
}
Err(err) => {
println!("Error: {err:#}");
true
match readline {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str())?;
let should_continue = Handle::current().block_on(async {
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => true,
Ok(ExitResult::Exit) => {
println!("Exiting ...");
false
}
Err(err) => {
println!("Error: {err:#}");
true
}
}
});
if !should_continue {
break;
}
});
if !should_continue {
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
break;
}
Err(err) => {
println!("Error: {err:#}");
break;
}
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
break;
}
Err(err) => {
println!("Error: {err:#}");
break;
}
}
}
rl.save_history(".dc-history.txt")?;
println!("history saved");
Ok::<_, Error>(())
});
rl.save_history(".dc-history.txt")?;
println!("history saved");
Ok::<_, Error>(())
})?;
context.stop_io().await;
input_loop.await??;
@@ -481,6 +485,7 @@ async fn handle_cmd(
#[tokio::main]
async fn main() -> Result<(), Error> {
console_subscriber::init();
let _ = pretty_env_logger::try_init();
let args = std::env::args().collect();

View File

@@ -79,7 +79,7 @@ class Rpc:
if self.closing:
return
event = await self.get_next_event()
account_id = event["context_id"]
account_id = event["contextId"]
queue = await self.get_queue(account_id)
await queue.put(event["event"])

View File

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

View File

@@ -41,6 +41,7 @@ skip = [
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.114.0"
"version": "1.115.0"
}

View File

@@ -74,7 +74,9 @@ Developing the bindings
If you want to develop or debug the bindings,
you can create a testing development environment using `tox`::
tox -c python --devenv env
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=debug
tox -c python --devenv env -e py
. env/bin/activate
Inside this environment the bindings are installed

View File

@@ -703,7 +703,7 @@ def test_mdn_asymmetric(acfactory, lp):
assert len(chat.get_messages()) == 1
# Wait for the message to be marked as seen on IMAP.
ac1._evtracker.get_info_contains("Marked messages 1 in folder DeltaChat as seen.")
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
# MDN is received even though MDNs are already disabled
assert msg_out.is_out_mdn_received()

View File

@@ -9,6 +9,7 @@ from deltachat.testplugin import (
create_dict_from_files_in_path,
write_dict_to_dir,
)
from deltachat.cutil import from_optional_dc_charpointer
# from deltachat.account import EventLogger
@@ -215,3 +216,19 @@ def test_logged_ac_process_ffi_failure(acfactory):
assert "ac_process_ffi_event" in res
assert "ZeroDivisionError" in res
assert "Traceback" in res
def test_jsonrpc_blocking_call(tmpdir):
accounts_fname = tmpdir.join("accounts")
accounts = ffi.gc(
lib.dc_accounts_new(ffi.NULL, accounts_fname.strpath.encode("ascii")),
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
res = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
)
assert res == "true"
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"

View File

@@ -1 +1 @@
2023-04-24
2023-05-12

View File

@@ -1,3 +0,0 @@
pre-release-commit-message = "chore({{crate_name}}): release {{version}}"
pro-release-commit-message = "chore({{crate_name}}): starting development cycle for {{next_version}}"
no-dev-version = true

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env python3
import sys
import subprocess
import set_core_version
from argparse import ArgumentParser
# update the version
parser = ArgumentParser(prog="release")
parser.add_argument("newversion")
try:
newversion = parser.parse_args().newversion
except SystemExit:
newversion = None
set_core_version.set_version(newversion)
tag = "v" + newversion
# TODO would be nice to automatically checkout the correct branch
# update the changelog
print(f"Updating CHANGELOG.md using git cliff --unreleased --tag {newversion} --prepend CHANGELOG.md")
changelog = subprocess.run(["git", "cliff", "--unreleased", "--tag", newversion, "--prepend", "CHANGELOG.md"]).stdout.strip()
subprocess.run(["git", "add", "-A"])
subprocess.run(["git", "commit", "-m", f"chore(release): prepare for {tag}"])
subprocess.run(["git", "show"])
# create a tag
subprocess.run(
[
"git", "tag", "-a", tag, "-m", f"Release {tag}", "-m", changelog
]
)
subprocess.run(["git", "tag", "-v", tag])
print("Done!")
print(f"Now push the commit (git push origin {tag}).")

View File

@@ -7,16 +7,10 @@ import pathlib
import re
import subprocess
from argparse import ArgumentParser
from pathlib import Path
rex = re.compile(r'version = "(\S+)"')
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
toml_list = [
"Cargo.toml",
"deltachat-ffi/Cargo.toml",
"deltachat-jsonrpc/Cargo.toml",
"deltachat-rpc-server/Cargo.toml",
"deltachat-repl/Cargo.toml",
]
def regex_matches(relpath, regex=rex):
p = pathlib.Path(relpath)
@@ -67,16 +61,31 @@ def update_package_json(relpath, newversion):
json.dump(json_data, f, sort_keys=True, indent=2)
f.write("\n")
def set_version(newversion):
if newversion is None:
def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
toml_list = [
"Cargo.toml",
"deltachat-ffi/Cargo.toml",
"deltachat-jsonrpc/Cargo.toml",
"deltachat-rpc-server/Cargo.toml",
"deltachat-repl/Cargo.toml",
]
try:
opts = parser.parse_args()
except SystemExit:
print()
for x in toml_list:
print(f"{x}: {read_toml_version(x)}")
for x in json_list:
print(f"{x}: {read_json_version(x)}")
print()
raise SystemExit("need argument: newversion, example: 1.25.0")
raise SystemExit("need argument: new version, example: 1.25.0")
newversion = opts.newversion
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
@@ -87,22 +96,14 @@ def set_version(newversion):
today = datetime.date.today().isoformat()
if "alpha" not in newversion:
changelog_name = "CHANGELOG.md"
changelog_tmpname = changelog_name + ".tmp"
changelog_tmp = open(changelog_tmpname, "w")
found = False
for line in open(changelog_name):
## 1.25.0
if line == f"## [{newversion}]\n":
line = f"## [{newversion}] - {today}\n"
for line in Path("CHANGELOG.md").open():
if line == f"## [{newversion}] - {today}\n":
found = True
changelog_tmp.write(line)
if not found:
raise SystemExit(
f"{changelog_name} contains no entry for version: {newversion}"
)
changelog_tmp.close()
os.rename(changelog_tmpname, changelog_name)
for toml_filename in toml_list:
replace_toml_version(toml_filename, newversion)
@@ -120,22 +121,15 @@ def set_version(newversion):
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
print("after commit, on master make sure to: ")
print("")
print("After commit, make sure to:")
print()
print(f" git tag -a v{newversion}")
print(f" git push origin v{newversion}")
print("")
def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
try:
newversion = parser.parse_args().newversion
except SystemExit:
newversion = None
set_version(newversion)
print(f" gh release create v{newversion} -n ''")
print()
print("Merge release branch into `master` if the release")
print("is made on a stable branch.")
print()
if __name__ == "__main__":

View File

@@ -5147,9 +5147,11 @@ mod tests {
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
assert_eq!(msg.match_indices("Gr.").count(), 2);
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 1);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Gr.").count(), 1);
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();

View File

@@ -181,7 +181,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let socks5_enabled = socks5_config.is_some();
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
let update_device_chats_handle = tokio::task::Builder::new()
.name("update_device_chats")
.spawn(async move { ctx2.update_device_chats().await })?;
// Step 1: Load the parameters and check email-address and password
@@ -350,44 +352,46 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
.provider
.map_or(socks5_config.is_some(), |provider| provider.opt.strict_tls);
let smtp_config_task = task::spawn(async move {
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user = smtp_server.username.clone();
smtp_param.server = smtp_server.hostname.clone();
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
smtp_param.certificate_checks = match smtp_server.strict_tls {
Some(true) => CertificateChecks::Strict,
Some(false) => CertificateChecks::AcceptInvalidCertificates,
None => CertificateChecks::Automatic,
};
let smtp_config_task = tokio::task::Builder::new()
.name("smtp_config")
.spawn(async move {
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user = smtp_server.username.clone();
smtp_param.server = smtp_server.hostname.clone();
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
smtp_param.certificate_checks = match smtp_server.strict_tls {
Some(true) => CertificateChecks::Strict,
Some(false) => CertificateChecks::AcceptInvalidCertificates,
None => CertificateChecks::Automatic,
};
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&socks5_config,
&smtp_addr,
provider_strict_tls,
&mut smtp,
)
.await
{
Ok(_) => {
smtp_configured = true;
break;
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&socks5_config,
&smtp_addr,
provider_strict_tls,
&mut smtp,
)
.await
{
Ok(_) => {
smtp_configured = true;
break;
}
Err(e) => errors.push(e),
}
Err(e) => errors.push(e),
}
}
if smtp_configured {
Ok(smtp_param)
} else {
Err(errors)
}
});
if smtp_configured {
Ok(smtp_param)
} else {
Err(errors)
}
})?;
progress!(ctx, 600);

View File

@@ -1555,7 +1555,10 @@ impl RecentlySeenLoop {
pub(crate) fn new(context: Context) -> Self {
let (interrupt_send, interrupt_recv) = channel::bounded(1);
let handle = task::spawn(Self::run(context, interrupt_recv));
let handle = tokio::task::Builder::new()
.name("recently_seen")
.spawn(Self::run(context, interrupt_recv))
.expect("failed to spawn recently_seen task");
Self {
handle,
interrupt_send,

View File

@@ -143,9 +143,11 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option<MsgId>) -> a
let (sender, debug_logging_recv) = channel::bounded(1000);
let loop_handle = {
let ctx = ctx.clone();
task::spawn(async move {
debug_logging_loop(&ctx, debug_logging_recv).await
})
tokio::task::Builder::new()
.name("debug_logging")
.spawn(async move {
debug_logging_loop(&ctx, debug_logging_recv).await
})?
};
*debug_logging = Some(DebugLogging {
msg_id,

View File

@@ -7,7 +7,6 @@
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
use std::future::Future;
use std::pin::Pin;

View File

@@ -1324,8 +1324,8 @@ impl Imap {
// Fetch last DC_FETCH_EXISTING_MSGS_COUNT (100) messages.
// Sequence numbers are sequential. If there are 1000 messages in the inbox,
// we can fetch the sequence numbers 900-1000 and get the last 100 messages.
let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT);
let set = format!("{first}:*");
let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT + 1);
let set = format!("{first}:{exists}");
let mut list = session
.fetch(&set, PREFETCH_FLAGS)
.await

View File

@@ -537,7 +537,9 @@ impl Message {
/// Returns the size of the file in bytes, if applicable.
pub async fn get_filebytes(&self, context: &Context) -> Result<Option<u64>> {
if let Some(path) = self.param.get_path(Param::File, context)? {
Ok(Some(get_filebytes(context, &path).await?))
Ok(Some(get_filebytes(context, &path).await.with_context(
|| format!("failed to get {} size in bytes", path.display()),
)?))
} else {
Ok(None)
}
@@ -1086,6 +1088,26 @@ impl MessageState {
}
}
/// Returns contacts that sent read receipts and the time of reading.
pub async fn get_msg_read_receipts(
context: &Context,
msg_id: MsgId,
) -> Result<Vec<(ContactId, i64)>> {
context
.sql
.query_map(
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
(msg_id,),
|row| {
let contact_id: ContactId = row.get(0)?;
let ts: i64 = row.get(1)?;
Ok((contact_id, ts))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await
}
/// Returns detailed message information in a multi-line text form.
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
let msg = Message::load_from_db(context, msg_id).await?;
@@ -1399,8 +1421,8 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
/// by moving them to the trash chat
/// and scheduling for deletion on IMAP.
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
for msg_id in msg_ids.iter() {
let msg = Message::load_from_db(context, *msg_id).await?;
for &msg_id in msg_ids {
let msg = Message::load_from_db(context, msg_id).await?;
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id).await?;
}
@@ -1410,7 +1432,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
.with_context(|| format!("Unable to trash message {msg_id}"))?;
if msg.viewtype == Viewtype::Webxdc {
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: *msg_id });
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
}
let target = context.get_delete_msgs_target().await?;
@@ -1430,7 +1452,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
.map(|dl| dl.msg_id);
if let Some(id) = logging_xdc_id {
if id == *msg_id {
if id == msg_id {
set_debug_logging_xdc(context, None).await?;
}
}

View File

@@ -15,6 +15,7 @@ use once_cell::sync::Lazy;
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::config::Config;
use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
use crate::contact::{addr_cmp, addr_normalize, ContactId};
use crate::context::Context;
@@ -1114,15 +1115,22 @@ impl MimeMessage {
(simplified_txt, top_quote)
};
// Truncate text if it has too many lines
let (simplified_txt, was_truncated) = truncate_by_lines(
simplified_txt,
DC_DESIRED_TEXT_LINES,
DC_DESIRED_TEXT_LINE_LEN,
);
if was_truncated {
self.is_mime_modified = was_truncated;
}
let is_bot = context.get_config_bool(Config::Bot).await?;
let simplified_txt = if is_bot {
simplified_txt
} else {
// Truncate text if it has too many lines
let (simplified_txt, was_truncated) = truncate_by_lines(
simplified_txt,
DC_DESIRED_TEXT_LINES,
DC_DESIRED_TEXT_LINE_LEN,
);
if was_truncated {
self.is_mime_modified = was_truncated;
}
simplified_txt
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
let mut part = Part {
@@ -3290,24 +3298,37 @@ On 2020-10-25, Bob wrote:
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mime_modified_large_plain() {
async fn test_mime_modified_large_plain() -> Result<()> {
let t = TestContext::new_alice().await;
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None)
.await
.unwrap();
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
assert!(mimemsg.is_mime_modified);
assert!(
mimemsg.parts[0].msg.matches("just repeated").count()
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
{
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?;
assert!(mimemsg.is_mime_modified);
assert!(
mimemsg.parts[0].msg.matches("just repeated").count()
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
}
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);
assert_eq!(
format!("{}\n", mimemsg.parts[0].msg),
REPEAT_TXT.repeat(REPEAT_CNT)
);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -364,17 +364,19 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
let passphrase = passphrase.to_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(&mut rng, s2k, Default::default(), || passphrase)?;
tokio::task::Builder::new()
.name("symm_encrypt")
.spawn_blocking(move || {
let mut rng = thread_rng();
let s2k = StringToKey::new_default(&mut rng);
let msg =
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?;
let encoded_msg = msg.to_armored_string(None)?;
let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg)
})
.await?
Ok(encoded_msg)
})?
.await?
}
/// Symmetric decryption.
@@ -385,20 +387,22 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
let (enc_msg, _) = Message::from_armor_single(ctext)?;
let passphrase = passphrase.to_string();
tokio::task::spawn_blocking(move || {
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
tokio::task::Builder::new()
.name("symm_decrypt")
.spawn_blocking(move || {
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
if let Some(msg) = msgs.first() {
match msg.get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
if let Some(msg) = msgs.first() {
match msg.get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
} else {
bail!("No valid messages found")
}
} else {
bail!("No valid messages found")
}
})
.await?
})?
.await?
}
#[cfg(test)]

View File

@@ -14,6 +14,7 @@
//! possible to remove all reactions by sending an empty string as a reaction,
//! even though RFC 9078 requires at least one emoji to be sent.
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fmt;
@@ -116,10 +117,9 @@ impl Reactions {
pub fn is_empty(&self) -> bool {
self.reactions.is_empty()
}
}
impl fmt::Display for Reactions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// Returns a map from emojis to their frequencies.
pub fn emoji_frequencies(&self) -> BTreeMap<String, usize> {
let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
for reaction in self.reactions.values() {
for emoji in reaction.emojis() {
@@ -129,6 +129,30 @@ impl fmt::Display for Reactions {
.or_insert(1);
}
}
emoji_frequencies
}
/// Returns a vector of emojis
/// sorted in descending order of frequencies.
///
/// This function can be used to display the reactions in
/// the message bubble in the UIs.
pub fn emoji_sorted_by_frequency(&self) -> Vec<(String, usize)> {
let mut emoji_frequencies: Vec<(String, usize)> =
self.emoji_frequencies().into_iter().collect();
emoji_frequencies.sort_by(|(a, a_count), (b, b_count)| {
match a_count.cmp(b_count).reverse() {
Ordering::Equal => a.cmp(b),
other => other,
}
});
emoji_frequencies
}
}
impl fmt::Display for Reactions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let emoji_frequencies = self.emoji_sorted_by_frequency();
let mut first = true;
for (emoji, frequency) in emoji_frequencies {
if !first {
@@ -481,6 +505,11 @@ Content-Disposition: reaction\n\
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍2 😀1");
assert_eq!(
reactions.emoji_sorted_by_frequency(),
vec![("👍".to_string(), 2), ("😀".to_string(), 1)]
);
Ok(())
}

View File

@@ -2084,7 +2084,7 @@ async fn check_verified_properties(
) -> Result<()> {
let contact = Contact::load_from_db(context, from_id).await?;
ensure!(mimeparser.was_encrypted(), "This message is not encrypted.");
ensure!(mimeparser.was_encrypted(), "This message is not encrypted");
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
// we do not fail here currently, this would exclude (a) non-deltas
@@ -2117,7 +2117,7 @@ async fn check_verified_properties(
if let Some(peerstate) = peerstate {
ensure!(
peerstate.has_verified_key(&mimeparser.signatures),
"The message was sent with non-verified encryption."
"The message was sent with non-verified encryption"
);
}
}

View File

@@ -740,7 +740,9 @@ impl Scheduler {
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
let handle = {
let ctx = ctx.clone();
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
tokio::task::Builder::new()
.name("inbox_loop")
.spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))?
};
let inbox = SchedBox {
meaning: FolderMeaning::Inbox,
@@ -760,7 +762,9 @@ impl Scheduler {
let (conn_state, handlers) = ImapConnectionState::new(&ctx).await?;
let (start_send, start_recv) = oneshot::channel();
let ctx = ctx.clone();
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
let handle = tokio::task::Builder::new()
.name("simple_imap_loop")
.spawn(simple_imap_loop(ctx, start_send, handlers, meaning))?;
oboxes.push(SchedBox {
meaning,
conn_state,
@@ -772,22 +776,28 @@ impl Scheduler {
let smtp_handle = {
let ctx = ctx.clone();
task::spawn(smtp_loop(ctx, smtp_start_send, smtp_handlers))
tokio::task::Builder::new()
.name("smtp_loop")
.spawn(smtp_loop(ctx, smtp_start_send, smtp_handlers))?
};
start_recvs.push(smtp_start_recv);
let ephemeral_handle = {
let ctx = ctx.clone();
task::spawn(async move {
ephemeral::ephemeral_loop(&ctx, ephemeral_interrupt_recv).await;
})
tokio::task::Builder::new()
.name("ephemeral")
.spawn(async move {
ephemeral::ephemeral_loop(&ctx, ephemeral_interrupt_recv).await;
})?
};
let location_handle = {
let ctx = ctx.clone();
task::spawn(async move {
location::location_loop(&ctx, location_interrupt_recv).await;
})
tokio::task::Builder::new()
.name("location")
.spawn(async move {
location::location_loop(&ctx, location_interrupt_recv).await;
})?
};
let recently_seen_loop = RecentlySeenLoop::new(ctx.clone());

View File

@@ -8,7 +8,6 @@ use anyhow::{bail, format_err, Context as _, Error, Result};
use async_smtp::response::{Category, Code, Detail};
use async_smtp::{self as smtp, EmailAddress, SmtpTransport};
use tokio::io::BufStream;
use tokio::task;
use crate::config::Config;
use crate::contact::{Contact, ContactId};
@@ -60,7 +59,10 @@ impl Smtp {
// Closing connection with a QUIT command may take some time, especially if it's a
// stale connection and an attempt to send the command times out. Send a command in a
// separate task to avoid waiting for reply or timeout.
task::spawn(async move { transport.quit().await });
tokio::task::Builder::new()
.name("smtp_disconnect")
.spawn(async move { transport.quit().await })
.expect("failed to spawn smtp_disconnect task");
}
self.last_success = None;
}
@@ -545,11 +547,11 @@ pub(crate) async fn send_msg_to_smtp(
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
.await
.context("failed to remove message with exceeded retry limit from smtp table")?;
bail!("Number of retries exceeded the limit");
return Ok(());
}
info!(
context,
"Try number {} to send message {} over SMTP", retries, msg_id
"Try number {retries} to send message {msg_id} (entry {rowid}) over SMTP"
);
let recipients_list = recipients
@@ -573,8 +575,13 @@ pub(crate) async fn send_msg_to_smtp(
{
info!(
context,
"Sending of message {} was cancelled by the user.", msg_id
"Sending of message {msg_id} (entry {rowid}) was cancelled by the user."
);
context
.sql
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
.await
.context("failed to remove cancelled message from smtp table")?;
return Ok(());
}
@@ -646,6 +653,8 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
},
)
.await?;
info!(context, "Selected rows from SMTP queue: {rowids:?}.");
for rowid in rowids {
send_msg_to_smtp(context, connection, rowid)
.await

View File

@@ -64,9 +64,11 @@ impl Smtp {
if let Some(ref mut transport) = self.transport {
transport.send(mail).await.map_err(Error::SmtpSend)?;
context.emit_event(EventType::SmtpMessageSent(format!(
"Message len={message_len_bytes} was smtp-sent to {recipients_display}"
)));
let info_msg = format!(
"Message len={message_len_bytes} was SMTP-sent to {recipients_display}"
);
info!(context, "{info_msg}.");
context.emit_event(EventType::SmtpMessageSent(info_msg));
self.last_success = Some(std::time::SystemTime::now());
} else {
warn!(

View File

@@ -711,6 +711,24 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
)
.await?;
}
if dbversion < 101 {
// Recreate `smtp` table with autoincrement.
// rfc724_mid index is not recreated, because it is not used.
sql.execute_migration(
"DROP TABLE smtp;
CREATE TABLE smtp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rfc724_mid TEXT NOT NULL, -- Message-ID
mime TEXT NOT NULL, -- SMTP payload
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
recipients TEXT NOT NULL, -- List of recipients separated by space
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
);
",
101,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)

View File

@@ -50,6 +50,9 @@ pub struct Summary {
/// Message state.
pub state: MessageState,
/// Message preview image path
pub thumbnail_path: Option<String>,
}
impl Summary {
@@ -90,11 +93,22 @@ impl Summary {
text = stock_str::reply_noun(context).await
}
let thumbnail_path = if msg.viewtype == Viewtype::Image
|| msg.viewtype == Viewtype::Gif
|| msg.viewtype == Viewtype::Sticker
{
msg.get_file(context)
.and_then(|path| path.to_str().map(|p| p.to_owned()))
} else {
None
};
Self {
prefix,
text,
timestamp: msg.get_timestamp(),
state: msg.state,
thumbnail_path,
}
}

View File

@@ -4,6 +4,7 @@ use std::convert::TryFrom;
use std::path::Path;
use anyhow::{anyhow, bail, ensure, format_err, Result};
use deltachat_derive::FromSql;
use lettre_email::mime;
use lettre_email::PartBuilder;
@@ -143,17 +144,25 @@ struct StatusUpdates {
/// Update items as sent on the wire and as stored in the database.
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct StatusUpdateItem {
pub(crate) payload: Value,
pub struct StatusUpdateItem {
/// The playload of the status update.
pub payload: Value,
/// Optional short info message that will be displayed in the chat.
/// For example "Alice added an item" or "Bob voted for option x".
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) info: Option<String>,
pub info: Option<String>,
/// The new name of the editing document.
/// This is not needed if the webxdc doesn't edit documents.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) document: Option<String>,
pub document: Option<String>,
/// Optional summary of the status update which will be shown next to the
/// app icon. This should be short and can be something like "8 votes"
/// for a voting app.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) summary: Option<String>,
pub summary: Option<String>,
}
/// Update items as passed to the UIs.
@@ -230,14 +239,14 @@ impl Context {
let valid = match async_zip::read::fs::ZipFileReader::new(path).await {
Ok(archive) => {
if find_zip_entry(archive.file(), "index.html").is_none() {
info!(self, "{} misses index.html", filename);
warn!(self, "{} misses index.html", filename);
false
} else {
true
}
}
Err(_) => {
info!(self, "{} cannot be opened as zip-file", filename);
warn!(self, "{} cannot be opened as zip-file", filename);
false
}
};
@@ -289,23 +298,11 @@ impl Context {
async fn create_status_update_record(
&self,
instance: &mut Message,
update_str: &str,
status_update_item: StatusUpdateItem,
timestamp: i64,
can_info_msg: bool,
from_id: ContactId,
) -> Result<StatusUpdateSerial> {
let update_str = strip_rtlo_characters(update_str.trim());
if update_str.is_empty() {
bail!("create_status_update_record: empty update.");
}
let status_update_item: StatusUpdateItem =
if let Ok(item) = serde_json::from_str::<StatusUpdateItem>(&update_str) {
item
} else {
bail!("create_status_update_record: no valid update item.");
};
if can_info_msg {
if let Some(ref info) = status_update_item.info {
if let Some(info_msg_id) =
@@ -404,6 +401,26 @@ impl Context {
instance_msg_id: MsgId,
update_str: &str,
descr: &str,
) -> Result<()> {
let status_update_item: StatusUpdateItem =
if let Ok(item) = serde_json::from_str::<StatusUpdateItem>(update_str) {
item
} else {
bail!("create_status_update_record: no valid update item.");
};
self.send_webxdc_status_update_struct(instance_msg_id, status_update_item, descr)
.await?;
Ok(())
}
/// Sends a status update for an webxdc instance.
/// Also see [Self::send_webxdc_status_update]
pub async fn send_webxdc_status_update_struct(
&self,
instance_msg_id: MsgId,
status_update: StatusUpdateItem,
descr: &str,
) -> Result<()> {
let mut instance = Message::load_from_db(self, instance_msg_id).await?;
if instance.viewtype != Viewtype::Webxdc {
@@ -420,10 +437,10 @@ impl Context {
MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft
);
let status_update_serial = self
let status_update_serial: StatusUpdateSerial = self
.create_status_update_record(
&mut instance,
update_str,
status_update,
create_smeared_timestamp(self),
send_now,
ContactId::SELF,
@@ -552,7 +569,7 @@ impl Context {
for update_item in updates.updates {
self.create_status_update_record(
&mut instance,
&serde_json::to_string(&update_item)?,
update_item,
timestamp,
can_info_msg,
from_id,
@@ -801,6 +818,8 @@ impl Message {
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::chat::{
add_contact_to_chat, create_broadcast_list, create_group_chat, forward_msgs,
@@ -1235,7 +1254,12 @@ mod tests {
let update_id1 = t
.create_status_update_record(
&mut instance,
"\n\n{\"payload\": {\"foo\":\"bar\"}}\n",
StatusUpdateItem {
payload: json!({"foo": "bar"}),
info: None,
document: None,
summary: None,
},
1640178619,
true,
ContactId::SELF,
@@ -1248,19 +1272,15 @@ mod tests {
);
assert!(t
.create_status_update_record(&mut instance, "\n\n\n", 1640178619, true, ContactId::SELF)
.send_webxdc_status_update(instance.id, "\n\n\n", "")
.await
.is_err());
assert!(t
.create_status_update_record(
&mut instance,
"bad json",
1640178619,
true,
ContactId::SELF
)
.send_webxdc_status_update(instance.id, "bad json", "")
.await
.is_err());
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
@@ -1270,7 +1290,12 @@ mod tests {
let update_id2 = t
.create_status_update_record(
&mut instance,
r#"{"payload" : { "foo2":"bar2"}}"#,
StatusUpdateItem {
payload: json!({"foo2": "bar2"}),
info: None,
document: None,
summary: None,
},
1640178619,
true,
ContactId::SELF,
@@ -1282,7 +1307,12 @@ mod tests {
);
t.create_status_update_record(
&mut instance,
r#"{"payload":true}"#,
StatusUpdateItem {
payload: Value::Bool(true),
info: None,
document: None,
summary: None,
},
1640178619,
true,
ContactId::SELF,
@@ -1296,15 +1326,12 @@ mod tests {
{"payload":true,"serial":3,"max_serial":3}]"#
);
let _update_id3 = t
.create_status_update_record(
&mut instance,
r#"{"payload" : 1, "sender": "that is not used"}"#,
1640178619,
true,
ContactId::SELF,
)
.await?;
t.send_webxdc_status_update(
instance.id,
r#"{"payload" : 1, "sender": "that is not used"}"#,
"",
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, update_id2).await?,
r#"[{"payload":true,"serial":3,"max_serial":4},