Compare commits

..

72 Commits

Author SHA1 Message Date
adbenitez
1f4b4bf201 fix: remove optional dependency aiodns
it fails to install in android(termux) because binary code dependency
2023-08-29 21:56:04 +02:00
link2xt
9a7d1faf75 Merge tag 'v1.120.0' 2023-08-28 11:55:53 +00:00
link2xt
e59c4ee858 Merge branch 'stable' 2023-08-27 22:18:36 +00:00
link2xt
a520f0268f chore: rustfmt 2023-08-26 18:15:11 +00:00
link2xt
5e3b1fa540 Merge branch 'stable' 2023-08-26 18:12:13 +00:00
iequidoo
95f29f7b63 fix: receive_imf: Set protection only for Chattype::Single (#4597)
Also don't set protection to ProtectionBroken if it already is.

Co-authored-by: Hocuri <hocuri@gmx.de>
2023-08-26 13:19:48 -03:00
link2xt
20513475ef Merge branch 'stable' 2023-08-25 01:59:04 +00:00
link2xt
3b806320ec Merge branch 'stable' 2023-08-24 22:25:02 +00:00
dependabot[bot]
c857d6e1bd Merge pull request #4591 from deltachat/dependabot/cargo/sanitize-filename-0.5.0 2023-08-24 20:01:21 +00:00
dependabot[bot]
94cd9a713f chore(cargo): bump sanitize-filename from 0.4.0 to 0.5.0
Bumps [sanitize-filename](https://github.com/kardeiz/sanitize-filename) from 0.4.0 to 0.5.0.
- [Commits](https://github.com/kardeiz/sanitize-filename/commits)

---
updated-dependencies:
- dependency-name: sanitize-filename
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-24 18:42:54 +00:00
link2xt
7676473ebd fix: do not mark non-verified group chats as verified when using securejoin
Only mark the chat is verified if 1:1 verified chats are enabled
and securejoin targets a 1:1 chat.
2023-08-24 18:39:56 +00:00
link2xt
8c778b3f5c test: extend test_qr_join_chat to check that the group is not verified 2023-08-24 18:39:56 +00:00
iequidoo
a66f8bd9fc fix: Delete messages from SMTP queue only on user demand (#4579)
I.e. from delete_msgs(). Otherwise messages must not be deleted from there, e.g. if a message is
ephemeral, but a network outage lasts longer than the ephemeral message timer, the message still
must be sent upon a successful reconnection.
2023-08-24 13:39:22 -03:00
iequidoo
95b2a15930 fix: Sort old incoming messages below all outgoing ones (#4621)
If the Inbox is fetched before the Sentbox (as done currently), messages from the Sentbox will
correctly mingle with the Inbox messages in the end. So, this commit changes message ordering only
if we already have processed outgoing messages, e.g. if we just sent them in the chat as described
in #4621. Otherwise new incoming messages are displayed somewhere in the middle of the chat which
doesn't look usable.
2023-08-24 13:22:26 -03:00
iequidoo
0179ec2da9 fix: receive_imf: Update peerstate from db after handling Securejoin handshake (#4600)
Otherwise has_verified_encryption() would check a stale Peerstate object.
2023-08-24 01:29:25 +00:00
iequidoo
8f2313bb2a test: test_openrpc_command_line: Check that deltachat-rpc-server exists with 0 2023-08-23 19:30:14 +00:00
link2xt
488a3d1118 build(deny): ignore RUSTSEC-2023-0052 2023-08-23 19:30:14 +00:00
link2xt
9094df7bc7 build(python): pin sphinx to 7.1.2 2023-08-23 19:30:14 +00:00
link2xt
16aad3fa67 build(cargo-deny): ignore RUSTSEC-2022-0093
It is an API issue that can only be fixed in rPGP and iroh upstream.
2023-08-17 12:20:58 +00:00
iequidoo
3b47c3f21d ci: Run Rust tests with RUST_BACKTRACE set 2023-08-15 14:24:02 -03:00
link2xt
987ce58926 chore(python): fix lint errors 2023-08-11 17:06:15 +00:00
iequidoo
20c88743df test: W/a message reordering in test_reaction_to_partially_fetched_msg() 2023-08-08 21:02:41 -03:00
dependabot[bot]
03395b95cb chore(cargo): bump quick-xml from 0.29.0 to 0.30.0
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.29.0 to 0.30.0.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-08 21:02:41 -03:00
Hocuri
53f04a134a test: Don't accidentally accept that a chat protection is broken (#4550) 2023-08-08 11:35:38 +02:00
Hocuri
885f26ea8c test: Directly unwrap in TestContext::get_chat() (#4614)
Directly unwrap in TestContext::get_chat()

Turns out that all usages of get_chat() directly unwrapped, because in a
test it doesn't make sense to handle the error of there being no chat.
2023-08-08 11:34:52 +02:00
link2xt
3ab181fdf8 build: update to Zig 0.11.0 2023-08-06 19:44:48 +00:00
link2xt
e12044e6af api!(deltachat-jsonrpc): use kind as a tag for all union types 2023-08-03 23:46:24 +00:00
link2xt
954067eb6d Merge tag 'v1.119.0' 2023-08-03 17:05:00 +00:00
link2xt
aecbebd566 docs: improve JSON-RPC API documentation 2023-08-03 16:06:36 +00:00
link2xt
3111bcde5e refactor: flatten imports in deltachat-jsonrpc 2023-08-03 15:58:48 +00:00
link2xt
e6cffd537e refactor: remove Chattype::Undefined 2023-08-02 17:02:28 +00:00
link2xt
70000d9ebb build: increase MSRV to 1.67.0
This is required by time v0.3.24
2023-08-02 03:27:21 +00:00
link2xt
d95843b0bf chore(deps): update dependencies 2023-08-02 03:23:19 +00:00
link2xt
13e766bc37 feat(deltachat-rpc-server): add --openrpc option 2023-08-01 18:27:02 +00:00
link2xt
c34edc582e test: test that get_system_info() works over RPC backup import 2023-08-01 01:05:06 +00:00
link2xt
8eee389c09 refactor: use SQL transaction in MsgId.delete_from_db() 2023-07-31 20:01:35 +00:00
link2xt
8ed6d4d709 api!: make MsgId.delete_from_db() private
Use `delete_msgs()` if you are using the Delta Chat core
as a library and want to delete a message.
2023-07-31 20:01:35 +00:00
Hocuri
60bacbec47 feat: Don't show a contact as verified if their key changed since the verification (#4574)
Don't show a contact as verified if their key changed in the meantime

If a contact's key changed since the verification, then it's very
unlikely that they still have the old, verified key. So, don't show them
as verified anymore.

This also means that you can't add a contact like this to a verified
group, which is good.

The documentation actually already described this (new) behavior:

```rust
/// and if the key has not changed since this verification.
```

so, this adapts the code to the documentation.
2023-07-31 18:59:45 +02:00
link2xt
af013559de refactor: hide DcSecretKey trait from the API 2023-07-29 18:10:25 +00:00
link2xt
b784415c57 refactor: move dc_preconfigure_keypair() implementation into deltachat crate
This allows to hide `DcKey` trait from public API.
2023-07-29 18:05:05 +00:00
link2xt
85739ba6ad refactor: make last_added_location_id an Option 2023-07-29 17:45:15 +00:00
B. Petersen
a02a593f47 fix example; this was changed some time ago, see https://docs.webxdc.org/spec.html#sendupdate 2023-07-29 05:08:54 +02:00
link2xt
67f28f501a Merge branch 'stable' 2023-07-27 19:40:01 +00:00
link2xt
9b9703a48e refactor: replace DcKey.load_self trait method with functions 2023-07-27 18:23:56 +00:00
link2xt
c55a3d3873 refactor: flatten and simplify imports 2023-07-27 17:47:30 +00:00
iequidoo
f27d304f3b feat!: Add lockfile to account manager (#4310)
Opening the same account (context) from multiple processes is dangerous, can result in duplicate
downloads of the same message etc. Same for account manager, attempts to modify the same
accounts.toml even if done atomically with may result in corrupted files as atomic replacement
procedure does not expect that multiple processes may write to the same temporary file.

accounts.toml cannot be used as a lockfile because it is replaced during atomic update. Therefore, a
new file next to accounts.toml is needed to prevent starting second account manager in the same
directory.

But iOS needs to be able to open accounts from multiple processes at the same time. This is required
as the "share-to-DC extension" is a separate process by iOS design -- this process may or may not be
started while the main app is running. Accounts are not altered however by this extension, so let's
add to the `Accounts::new()` constructor an `rdwr` parameter which allows to read the accounts
config w/o locking the lockfile.
2023-07-26 16:02:25 -03:00
link2xt
6d51d19f01 refactor(e2ee): do not return anything from ensure_secret_key_exists()
The return value was never used.
2023-07-26 11:19:08 +00:00
Hocuri
170968dfc2 Update README.md 2023-07-25 16:50:10 +02:00
link2xt
f930576fd1 Merge branch 'stable' 2023-07-24 18:40:44 +00:00
link2xt
d797de7a8d refactor: use slices and vectors instead of Keyring wrapper
This change removes all traces of dc_keyring_t,
which was a C implementation of dynamically sized array.
2023-07-24 18:05:38 +00:00
link2xt
acc7bb00c5 chore(deps): update rPGP 2023-07-24 16:14:16 +00:00
link2xt
8fb8a877be chore(deps): update dependencies 2023-07-24 14:06:09 +00:00
Hocuri
b96028cd87 api!(Rust): Remove unused function is_verified_ex() (#4551)
No one used it anymore, and all occurences I could find (on
GitHub)[https://github.com/search?q=%22is_verified_ex%22&type=code&p=1]
are either forks of deltachat-core-rust or of deltachat-core (which is
the old C core).
2023-07-24 12:19:13 +02:00
Hocuri
682e241edb fix: Fix info-message orderings of verified 1:1 chats (#4545)
Correctly handle messages with old timestamps for verified chats:

 * They must not be sorted over a protection-changed info message

 * If they change the protection, then they must not be sorted over existing other messages, because then the protection-changed info message would also be above these existing messages.


This PR fixes this:

 1. Even seen messages can't be sorted into already-noticed messages anymore. **This also changes DC's behavior in the absence of verified 1:1 chats**. Before this PR, messages that are marked as seen when they are downloaded will always be sorted by their timestamp, even if it's very old.

 2. protection-changed info messages are always sorted to the bottom.

    **Edit:**

 3. There is an exception to rule 1: Outgoing messages are still allowed to be sorted purely by their timestamp, and don't influence old messages. This is to the problem described at [*].


Together, these rules also make sure that the protection-changed info message is always right above the message causing the change.

[*] If we receive messages from two different folders, e.g. `Sent` and `Inbox`, then this will lead to wrong message ordering in many cases. I need to think about this more, or maybe someone else has an idea. One new idea that came to my mind is:

 * Always sort noticed messages under the newest info message (this PR sorts them under the newest noticed message, master sorts them purely by their sent timestamp)

 * Always sort unnoticed messages under the newest noticed message (that's the same behavior as in this PR and on master)

 * Always sort protection-changed info messages to the bottom (as in this PR)


However, after a talk with @link2xt we instead decided to add rule 3. (see above) because it seemed a little bit easier.
2023-07-24 12:16:32 +02:00
Simon Laux
3a63628f1f update node constants
looks like this was fogotten when changing the chat protection stock strings
2023-07-23 09:26:01 +00:00
link2xt
3705616cd9 Merge branch 'stable' 2023-07-23 09:17:13 +00:00
Simon Laux
b8fcb660ad cargo fmt 2023-07-23 02:29:42 +02:00
Simon Laux
5673294623 api(jsonrpc): add resend_messages 2023-07-23 02:29:42 +02:00
B. Petersen
7062bb0502 clarify transitive behaviour of dc_contact_is_verfified() 2023-07-22 20:58:05 +02:00
link2xt
659cffe0cc ci: remove comment about python from rust tests 2023-07-19 13:43:20 +00:00
link2xt
a1663a98e0 build: use Rust 1.71.0 and increase MSRV to 1.66.0
Rust 1.66 is required by constant_time_eq 0.3.0.
2023-07-19 13:41:31 +00:00
link2xt
3de1dbc9e4 chore(deps): update dependencies 2023-07-19 13:26:47 +00:00
link2xt
6d37e8601e Merge branch 'stable' 2023-07-17 17:11:38 +00:00
Hocuri
d762753103 fix: Allow to save a draft if the verification is broken (#4542)
If the verification is broken, `can_send()` is false.

But if the user was typing a message right when a verification-breaking message came in, the UI still needs to be able to save it as a draft.

Steps to reproduce the bug:
  - Set a draft
  - Your chat partner breaks verification
  - Go back to the chats list
  - Go to the chat again
  - Accept the breakage
  - Expected: The draft is still there
  - Bug behavior: The draft is gone
2023-07-16 12:04:43 +02:00
link2xt
a020d5ccce Merge branch 'stable' 2023-07-14 11:23:43 +00:00
Hocuri
1e28ea9bb0 fix: Don't create 1:1 chat as protected for contact who doesn't prefer to encrypt (#4538) 2023-07-11 17:39:59 +00:00
Hocuri
17f2d33731 test: Remove unnecessary inner_set_protection() call (#4539)
1:1 chats are automatically created as protected if the contact is
verified, there is no need to explicitly do this.

Plus, by removing this call, the test also tests that automatically
creating 1:1 chats as protected works.
2023-07-11 19:15:23 +02:00
link2xt
976797d4cf build: remove examples/simple.rs
When `cargo test` is executed,
all examples are built by default
to ensure that they can be compiled.

This is a documented and expected behaviour,
even though it was previously reported as a bug:
<https://github.com/rust-lang/cargo/issues/6675>

In particular, `examples/simple.rs` is built into
a 67M binary `target/debug/examples/simple`.
This is unnecessary to do so every time
you change a line in the `deltachat` crate
and want to rerun the tests.

Workaround is to run `cargo test --tests`,
but it is easy to forget and is not discoverable
unless you read the "Target Selection" section of `cargo help test`.

We have a maintained example at https://github.com/deltachat-bot/echo,
so there is no need for an example in the core repository.
2023-07-10 21:49:31 +00:00
link2xt
31e3169433 chore: nightly clippy fixes 2023-07-10 11:38:46 +02:00
link2xt
d2b15cb629 docs: document how logs and error messages should be formatted 2023-07-09 16:18:18 +00:00
Hocuri
9cd000c4f2 feat: Verified 1:1 chats (#4315)
Implement #4188

BREAKING CHANGE: Remove unused DC_STR_PROTECTION_(EN)ABLED* strings
BREAKING CHANGE: Remove unused dc_set_chat_protection()
2023-07-09 14:06:45 +02:00
link2xt
243c035b03 chore: spellcheck 2023-07-07 21:56:59 +00:00
144 changed files with 5212 additions and 7224 deletions

View File

@@ -25,7 +25,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.73.0
RUSTUP_TOOLCHAIN: 1.72.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -39,6 +39,10 @@ jobs:
- name: Check
run: cargo check --workspace --all-targets --all-features
# Check with musl libc target which is used for `deltachat-rpc-server` releases.
- name: Check musl
run: scripts/zig-musl-check.sh
cargo_deny:
name: cargo deny
runs-on: ubuntu-latest
@@ -77,19 +81,15 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.73.0
rust: 1.71.0
- os: windows-latest
rust: 1.73.0
rust: 1.71.0
- os: macos-latest
rust: 1.73.0
rust: 1.71.0
# Minimum Supported Rust Version = 1.65.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
# Minimum Supported Rust Version = 1.67.0
- os: ubuntu-latest
rust: 1.65.0
rust: 1.67.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -102,6 +102,8 @@ jobs:
uses: swatinem/rust-cache@v2
- name: Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace
- name: Test cargo vendor
@@ -178,15 +180,15 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.12
python: 3.11
- os: macos-latest
python: 3.12
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.10
python: pypy3.9
- os: macos-latest
python: pypy3.10
python: pypy3.9
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
@@ -228,19 +230,22 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.12
python: 3.11
- os: macos-latest
python: 3.12
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.10
python: pypy3.9
- os: macos-latest
python: pypy3.10
python: pypy3.9
# Minimum Supported Python Version = 3.7
# 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

@@ -26,17 +26,35 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Install ziglang
run: pip install wheel ziglang==0.11.0
- name: Build deltachat-rpc-server binaries
- name: Build
run: sh scripts/zig-rpc-server.sh
- name: Upload dist directory with Linux binaries
- name: Upload x86_64 binary
uses: actions/upload-artifact@v3
with:
name: linux
path: dist/
name: deltachat-rpc-server-x86_64
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload i686 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-i686
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload aarch64 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-aarch64
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload armv7 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-armv7
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
if-no-files-found: error
build_windows:
@@ -74,87 +92,39 @@ jobs:
build_macos:
name: Build deltachat-rpc-server for macOS
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
- arch: aarch64
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup rust target
run: rustup target add ${{ matrix.arch }}-apple-darwin
run: rustup target add x86_64-apple-darwin
- name: Build
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
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-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
name: deltachat-rpc-server-x86_64-macos
path: target/x86_64-apple-darwin/release/deltachat-rpc-server
if-no-files-found: error
publish:
name: Build wheels and upload binaries to the release
name: Upload binaries to the release
needs: ["build_linux", "build_windows", "build_macos"]
permissions:
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- name: Download built binaries
uses: "actions/download-artifact@v3"
- name: Download Linux binaries
uses: actions/download-artifact@v3
with:
name: linux
path: dist/
- name: Download win32 binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-win32.exe
path: deltachat-rpc-server-win32.exe.d
- name: Download win64 binary
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-win64.exe
path: deltachat-rpc-server-win64.exe.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v3
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Flatten dist/ directory
- name: Compose dist/ directory
run: |
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v4
with:
python-version: 3.12
- name: Install wheel
run: pip install wheel
- name: Build deltachat-rpc-server Python wheels and source package
run: scripts/wheel-rpc-server.py
mkdir dist
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
- name: List downloaded artifacts
run: ls -l dist/

View File

@@ -67,9 +67,7 @@ jobs:
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
container: ubuntu:18.04
container: debian:10
steps:
# Working directory is owned by 1001:1001 by default.
# Change it to our user.

4
.gitignore vendored
View File

@@ -1,7 +1,6 @@
/target
**/*.rs.bk
/build
/dist
# ignore vi temporaries
*~
@@ -19,9 +18,6 @@ python/.eggs
__pycache__
python/src/deltachat/capi*.so
python/.venv/
python/venv/
venv/
env/
python/liveconfig*

View File

@@ -1,201 +1,5 @@
# Changelog
## [1.126.1] - 2023-10-24
### Fixes
- Do not hardcode version in deltachat-rpc-server source package.
- Do not interrupt IMAP loop from `get_connectivity_html()`.
### Features / Changes
- imap: Buffer `STARTTLS` command.
### Build system
- Build `deltachat-rpc-server` binary for aarch64 macOS.
- Build `deltachat-rpc-server` wheels for macOS and Windows.
### Refactor
- Remove job queue.
### Miscellaneous Tasks
- cargo: Update `ahash` to make `cargo-deny` happy.
## [1.126.0] - 2023-10-22
### API-Changes
- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)).
- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)).
### Features / Changes
- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)).
- This means that UIs need to ask for the name when creating a broadcast list, similar to <https://github.com/deltachat/deltachat-android/pull/2653>.
- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820))
### CI
- Build Python wheels for deltachat-rpc-server.
### Build system
- Strip release binaries.
- Workaround OpenSSL crate expecting libatomic to be available.
### Fixes
- Set `soft_heap_limit` on SQLite database.
- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT.
## [1.125.0] - 2023-10-14
### API-Changes
- [**breaking**] deltachat-rpc-client: Replace `asyncio` with threads.
- Validate boolean values passed to `set_config`. Attempts to set values other than `0` and `1` will result in an error.
### CI
- Reduce required Python version for deltachat-rpc-client from 3.8 to 3.7.
### Features / Changes
- Add developer option to disable IDLE.
### Fixes
- `deltachat-rpc-client`: Run `deltachat-rpc-server` in its own process group. This prevents reception of `SIGINT` by the server when the bot is terminated with `^C`.
- python: Don't automatically set the displayname to "bot" when setting log level.
- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/deltachat/deltachat-core-rust/pull/4700)).
- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/deltachat/deltachat-core-rust/pull/4757)).
- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/deltachat/deltachat-core-rust/pull/4811)).
- Set connectivity status to "connected" during fake idle.
- Return verifier contacts regardless of their origin.
- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/deltachat/deltachat-core-rust/pull/4534)).
### Refactor
- deltachat-rpc-client: Close stdin instead of sending `SIGTERM`.
- deltachat-rpc-client: Remove print() calls. Standard `logging` package is for logging instead.
### Tests
- deltachat-rpc-client: Enable logs in pytest.
## [1.124.1] - 2023-10-05
### Fixes
- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)).
### CI
- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788))
## [1.124.0] - 2023-10-04
### API-Changes
- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
- Deprecate `dc_contact_get_verifier_addr`.
- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
- Deprecate `get_next_media()`.
- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
### Fixes
- Wrap base64-encoded parts to 76 characters.
- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
- Do not put the status footer into reaction MIME parts.
- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
### Features / Changes
- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
- deltachat-rpc-client: Log exceptions when long-running tasks die.
### Build
- Build wheels for Python 3.12 and PyPy 3.10.
## [1.123.0] - 2023-09-22
### API-Changes
- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
### Fixes
- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
- imex: Use "default" in the filename of the default key.
### Miscellaneous Tasks
- Update OpenSSL from 3.1.2 to 3.1.3.
## [1.122.0] - 2023-09-12
### API-Changes
- jsonrpc: Return only chat IDs for similar chats.
### Fixes
- Reopen all connections on database passpharse change.
- Do not block new group chats if 1:1 chat is blocked.
- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
### Build system
- Update to OpenSSL 3.0.
- Bump webpki from 0.22.0 to 0.22.1.
- python: Add link to Mastodon into projects.urls.
### Features / Changes
- Add RSA-4096 key generation support.
### Refactor
- pgp: Add constants for encryption algorithm and hash.
## [1.121.0] - 2023-09-06
### API-Changes
- Add `dc_context_change_passphrase()`.
- Add `Message.set_file_from_bytes()` API.
- Add experimental API to get similar chats.
### Build system
- Build node packages on Ubuntu 18.04 instead of Debian 10.
This reduces the requirement for glibc version from 2.28 to 2.27.
### Fixes
- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
- Do not allow dots at the end of email addresses.
- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
`aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
## [1.120.0] - 2023-08-28
### API-Changes
@@ -2960,10 +2764,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0

View File

@@ -76,6 +76,29 @@ If you have multiple changes in one PR, create multiple conventional commits, an
[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}"))
```
### 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.

519
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.126.1"
version = "1.120.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"
rust-version = "1.67"
[profile.dev]
debug = 0
@@ -24,7 +24,6 @@ lto = true
panic = 'abort'
opt-level = "z"
codegen-units = 1
strip = true
[patch.crates-io]
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
@@ -49,6 +48,7 @@ email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.8"
fd-lock = "3.0.11"
futures = "0.3"
futures-lite = "1.13.0"
hex = "0.4.0"
@@ -69,13 +69,13 @@ parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.29"
quick-xml = "0.30"
rand = "0.8"
regex = "1.8"
reqwest = { version = "0.11.18", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.4"
sanitize-filename = "0.5"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
@@ -119,11 +119,6 @@ members = [
"format-flowed",
]
[[example]]
name = "simple"
path = "examples/simple.rs"
[[bench]]
name = "create_account"
harness = false

View File

@@ -1,8 +1,16 @@
# Delta Chat Rust
<p align="center">
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
</p>
> Deltachat-core written in Rust
<p align="center">
<a href="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml">
<img alt="Rust CI" src="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml/badge.svg">
</a>
</p>
[![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
<p align="center">
The core library for Delta Chat, written in Rust
</p>
## Installing Rust and Cargo

View File

@@ -8,7 +8,8 @@ async fn create_accounts(n: u32) {
let dir = tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
for expected_id in 2..n {
let id = accounts.add_account().await.unwrap();

View File

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

View File

@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
# exclude all test directories use the pattern */test/*
######################################################
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
EXCLUDE_SYMBOLS += _dc_* jsmn*
######################################################

View File

@@ -301,19 +301,6 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
int dc_context_open (dc_context_t *context, const char* passphrase);
/**
* Changes the passphrase on the open database.
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
* It is impossible to encrypt unencrypted database with this method and vice versa.
*
* @memberof dc_context_t
* @param context The context object.
* @param passphrase The new passphrase.
* @return 1 on success, 0 on error.
*/
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
/**
* Returns 1 if database is open.
*
@@ -443,9 +430,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Curve25519 keypair
* DC_KEY_GEN_RSA4096 (3)=
* generate RSA 4096 keypair
* generate Ed25519 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
@@ -492,9 +477,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
* 0=use IMAP IDLE if the server supports it.
* This is a developer option used for testing polling used as an IDLE fallback.
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
* For larger messages, only the header is downloaded and a placeholder is shown.
* These messages can be downloaded fully using dc_download_full_msg() later.
@@ -503,9 +485,13 @@ 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.
* - `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.
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
* to 1 if it supports verified 1:1 chats.
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
* and when the key changes, an info message is posted into the chat.
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
@@ -830,7 +816,7 @@ void dc_maybe_network (dc_context_t* context);
* @param context The context as created by dc_context_new().
* @param addr The e-mail address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data Ignored, actual public key is extracted from secret_data.
* @param public_data ASCII armored public key.
* @param secret_data ASCII armored secret key.
* @return 1 on success, 0 on failure.
*/
@@ -884,8 +870,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
* is added as needed.
* @param query_str An optional query for filtering the list. Only chats matching this query
* are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
* the chatlist is filtered such that only chats with unread messages show up.
* are returned. Give NULL for no filtering.
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
* are returned. Give 0 for no filtering.
* @return A chatlist as an dc_chatlist_t object.
@@ -1125,7 +1110,7 @@ dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
*
* In JS land, that would be mapped to something as:
* ```
* success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
* success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4');
* ```
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
* See dc_get_webxdc_status_updates() for the receiving counterpart.
@@ -1343,20 +1328,6 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
/**
* Returns a list of similar chats.
*
* @warning This is an experimental API which may change or be removed in the future.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat for which to find similar chats.
* @return The list of similar chats.
* On errors, NULL is returned.
* Must be freed using dc_chatlist_unref() when no longer used.
*/
dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id);
/**
* Estimate the number of messages that will be deleted
@@ -1488,7 +1459,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The ID of the current message from which the next or previous message should be searched.
@@ -1507,24 +1477,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
/**
* Enable or disable protection against active attacks.
* To enable protection, it is needed that all members are verified;
* if this condition is met, end-to-end-encryption is always enabled
* and only the verified keys are used.
*
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to change the protection for.
* @param protect 1=protect chat, 0=unprotect chat
* @return 1=success, 0=error, e.g. some members may be unverified
*/
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
/**
* Set chat visibility to pinned, archived or normal.
*
@@ -1718,12 +1670,24 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in a read-only chat
* and will see who the other members are.
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
*
* For historical reasons, this function does not take a name directly,
* instead you have to set the name using dc_set_chat_name()
* after creating the broadcast list.
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in the broadcast list
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
*
* @memberof dc_context_t
* @param context The context object.
@@ -2266,7 +2230,8 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
* the backup is not encrypted.
* The backup contains all contacts, chats, images and other data and device independent settings.
* The backup does not contain device dependent settings as ringtones or LED notification settings.
* The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
* the format is `delta-chat-<day>-<number>.tar`
*
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
@@ -2279,7 +2244,6 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
*
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
* If `param1` is a filename, import the private key from the file and make it the default.
*
* While dc_imex() returns immediately, the started job may take a while,
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
@@ -2951,12 +2915,15 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
* @param dir The directory to create the context-databases in.
* If the directory does not exist,
* dc_accounts_new() will try to create it.
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
* @return An account manager object.
* The object must be passed to the other account manager functions
* and must be freed using dc_accounts_unref() after usage.
* On errors, NULL is returned.
*/
dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir);
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
/**
@@ -3737,7 +3704,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
* Check if a chat is protected.
* Protected chats contain only verified members and encryption is always enabled.
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
* The status can be changed using dc_set_chat_protection().
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3746,6 +3712,26 @@ int dc_chat_can_send (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
*
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
* otherwise it will return false for all chats.
*
* 1:1 chats are automatically set as protected when a contact is verified.
* When a message comes in that is not encrypted / signed correctly,
* the chat is automatically set as unprotected again.
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
*
* The UI should let the user confirm that this is OK with a message like
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
*/
int dc_chat_is_protection_broken (const dc_chat_t* chat);
/**
* Check if locations are sent to the chat
* at the time the object was created using dc_get_chat().
@@ -3950,7 +3936,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
* Get the message time used for sorting.
* This function returns the timestamp that is used for sorting the message
* into lists as returned e.g. by dc_get_chat_msgs().
* This may be the received time, the sending time or another time.
* This may be the reveived time, the sending time or another time.
*
* To get the receiving time, use dc_msg_get_received_timestamp().
* To get the sending time, use dc_msg_get_timestamp().
@@ -4340,7 +4326,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
* Check if the message is an informational message, created by the
* device or by another users. Such messages are not "typed" by the user but
* created due to other actions,
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
* or dc_add_contact_to_chat().
*
* These messages are typically shown in the center of the chat view,
@@ -5034,7 +5020,12 @@ int dc_contact_is_verified (dc_contact_t* contact);
/**
* Return the address that verified a contact
*
* The UI may use this in addition to a checkmark showing the verification status
* The UI may use this in addition to a checkmark showing the verification status.
* In case of verification chains,
* the last contact in the chain is shown.
* This is because of privacy reasons, but also as it would not help the user
* to see a unknown name here - where one can mostly always ask the shown name
* as it is directly known.
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5042,7 +5033,6 @@ int dc_contact_is_verified (dc_contact_t* contact);
* A string containing the verifiers address. If it is the same address as the contact itself,
* we verified the contact ourself. If it is an empty string, we don't have verifier
* information or the contact is not verified.
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
*/
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
@@ -5055,7 +5045,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
* @memberof dc_contact_t
* @param contact The contact object.
* @return
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
* we verified the contact ourself. If it is 0, we don't have verifier information or
* the contact is not verified.
*/
@@ -5751,11 +5741,12 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param input JSON-RPC request.
* @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.
* If there is no response, NULL is returned.
* On error, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
/**
* @class dc_event_emitter_t
@@ -6291,7 +6282,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
#define DC_KEY_GEN_RSA4096 3
/**
@@ -6775,15 +6765,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in error strings.
#define DC_STR_ERROR_NO_NETWORK 87
/// "Chat protection enabled."
///
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
#define DC_STR_PROTECTION_ENABLED 88
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
#define DC_STR_PROTECTION_DISABLED 89
/// "Reply"
///
/// Used in summaries.
@@ -7228,26 +7209,6 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You enabled chat protection."
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
/// "Chat protection enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
/// "You disabled chat protection."
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
/// "Chat protection disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
@@ -7258,6 +7219,16 @@ void dc_event_unref(dc_event_t* event);
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/// "Messages are guaranteed to be end-to-end encrypted from now on."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
/// "%1$s sent a message from another device."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/**
* @}
*/

View File

@@ -29,7 +29,7 @@ use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::{DcKey, DcSecretKey};
use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
use deltachat::net::read_url_blob;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
@@ -167,24 +167,6 @@ pub unsafe extern "C" fn dc_context_open(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_change_passphrase(
context: *mut dc_context_t,
passphrase: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_change_passphrase()");
return 0;
}
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.change_passphrase(passphrase))
.context("dc_context_change_passphrase() failed")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -805,7 +787,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
_public_data: *const libc::c_char,
public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
@@ -813,21 +795,13 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
return 0;
}
let ctx = &*context;
block_on(async move {
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
let public = secret.split_public_key()?;
let keypair = key::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
Ok::<_, anyhow::Error>(1)
})
.context("Failed to save keypair")
.log_err(ctx)
.unwrap_or(0)
let addr = to_string_lossy(addr);
let public_data = to_string_lossy(public_data);
let secret_data = to_string_lossy(secret_data);
block_on(preconfigure_keypair(ctx, &addr, &public_data, &secret_data))
.context("Failed to save keypair")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -1260,30 +1234,6 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_similar_chatlist(
context: *mut dc_context_t,
chat_id: u32,
) -> *mut dc_chatlist_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_similar_chatlist()");
return ptr::null_mut();
}
let ctx = &*context;
let chat_id = ChatId::new(chat_id);
match block_on(chat_id.get_similar_chatlist(ctx))
.context("failed to get similar chatlist")
.log_err(ctx)
{
Ok(list) => {
let ffi_list = ChatlistWrapper { context, list };
Box::into_raw(Box::new(ffi_list))
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
context: *mut dc_context_t,
@@ -1431,7 +1381,6 @@ pub unsafe extern "C" fn dc_get_chat_media(
}
#[no_mangle]
#[allow(deprecated)]
pub unsafe extern "C" fn dc_get_next_media(
context: *mut dc_context_t,
msg_id: u32,
@@ -1472,32 +1421,6 @@ pub unsafe extern "C" fn dc_get_next_media(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_protection(
context: *mut dc_context_t,
chat_id: u32,
protect: libc::c_int,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_protection()");
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
return 0;
};
block_on(async move {
match ChatId::new(chat_id).set_protection(ctx, protect).await {
Ok(()) => 1,
Err(_) => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -2568,12 +2491,7 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(async move {
location::set(ctx, latitude, longitude, accuracy)
.await
.log_err(ctx)
.unwrap_or_default()
}) as libc::c_int
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
}
#[no_mangle]
@@ -3132,6 +3050,16 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
ffi_chat.chat.is_protected() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protection_broken() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
@@ -4537,14 +4465,7 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
true,
))
.log_err(ctx)
.unwrap_or_default()
{
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4571,14 +4492,11 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
match socks5_enabled {
Ok(socks5_enabled) => {
match block_on(provider::get_provider_info_by_addr(
match block_on(provider::get_provider_info(
ctx,
addr.as_str(),
socks5_enabled,
))
.log_err(ctx)
.unwrap_or_default()
{
)) {
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4750,17 +4668,17 @@ pub type dc_accounts_t = AccountsWrapper;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new(
_os_name: *const libc::c_char,
dbfile: *const libc::c_char,
dir: *const libc::c_char,
writable: libc::c_int,
) -> *mut dc_accounts_t {
setup_panic!();
if dbfile.is_null() {
if dir.is_null() {
eprintln!("ignoring careless call to dc_accounts_new()");
return ptr::null_mut();
}
let accs = block_on(Accounts::new(as_path(dbfile).into()));
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
match accs {
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
@@ -5026,7 +4944,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::*;
@@ -5102,24 +5020,25 @@ mod jsonrpc {
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
input: *const libc::c_char,
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 input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
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 {
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
}
None => ptr::null_mut(),
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.126.1"
version = "1.120.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -22,10 +22,10 @@ log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.99"
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.29.1" }
sanitize-filename = "0.4"
sanitize-filename = "0.5"
walkdir = "2.3.3"
base64 = "0.21"

View File

@@ -4,30 +4,30 @@ 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::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
},
chatlist::Chatlist,
config::Config,
constants::DC_MSG_ID_DAYMARKER,
contact::{may_be_valid_addr, Contact, ContactId, Origin},
context::get_info,
ephemeral::Timer,
imex, location,
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
provider::get_provider_info,
qr,
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
reaction::{get_msg_reactions, send_reaction},
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -142,11 +142,7 @@ impl CommandApi {
}
}
#[rpc(
all_positional,
ts_outdir = "typescript/generated",
openrpc_outdir = "openrpc"
)]
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
@@ -157,12 +153,12 @@ impl CommandApi {
// Misc top level functions
// ---------------------------------------------
/// Check if an email address is valid.
/// Checks if an email address is valid.
async fn check_email_validity(&self, email: String) -> bool {
may_be_valid_addr(&email)
}
/// Get general system info.
/// Returns general system info.
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
get_info()
}
@@ -223,11 +219,13 @@ impl CommandApi {
Ok(accounts)
}
/// Starts background tasks for all accounts.
async fn start_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.start_io().await;
Ok(())
}
/// Stops background tasks for all accounts.
async fn stop_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.stop_io().await;
Ok(())
@@ -237,14 +235,16 @@ impl CommandApi {
// Methods that work on individual accounts
// ---------------------------------------------
async fn start_io(&self, id: u32) -> Result<()> {
let ctx = self.get_context(id).await?;
/// Starts background tasks for a single account.
async fn start_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.start_io().await;
Ok(())
}
async fn stop_io(&self, id: u32) -> Result<()> {
let ctx = self.get_context(id).await?;
/// Stops background tasks for a single account.
async fn stop_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
Ok(())
}
@@ -311,11 +311,13 @@ impl CommandApi {
ctx.get_info().await
}
/// Sets the given configuration key.
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
set_config(&ctx, &key, value.as_deref()).await
}
/// Updates a batch of configuration values.
async fn batch_set_config(
&self,
account_id: u32,
@@ -347,6 +349,7 @@ impl CommandApi {
Ok(qr_object)
}
/// Returns configuration value for the given key.
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
get_config(&ctx, &key).await
@@ -566,21 +569,6 @@ impl CommandApi {
Ok(l)
}
/// Returns chats similar to the given one.
///
/// Experimental API, subject to change without notice.
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let list = chat_id
.get_similar_chat_ids(&ctx)
.await?
.into_iter()
.map(|(chat_id, _metric)| chat_id.to_u32())
.collect();
Ok(list)
}
async fn get_chatlist_items_by_entries(
&self,
account_id: u32,
@@ -812,12 +800,24 @@ impl CommandApi {
/// Create a new broadcast list.
///
/// Broadcast lists are similar to groups on the sending device,
/// however, recipients get the messages in a read-only chat
/// and will see who the other members are.
/// however, recipients get the messages in normal one-to-one chats
/// and will not be aware of other members.
///
/// For historical reasons, this function does not take a name directly,
/// instead you have to set the name using dc_set_chat_name()
/// after creating the broadcast list.
/// Replies to broadcasts go only to the sender
/// and not to all broadcast recipients.
/// Moreover, replies will not appear in the broadcast list
/// but in the one-to-one chat with the person answering.
///
/// The name and the image of the broadcast list is set automatically
/// and is visible to the sender only.
/// Not asking for these data allows more focused creation
/// and we bypass the question who will get which data.
/// Also, many users will have at most one broadcast list
/// so, a generic name and image is sufficient at the first place.
///
/// Later on, however, the name can be changed using dc_set_chat_name().
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
/// All in all, this is also what other messengers are doing here.
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast_list(&ctx)
@@ -1415,10 +1415,6 @@ impl CommandApi {
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
///
/// Deprecated 2023-10-03, use `get_chat_media` method
/// and navigate the returned array instead.
#[allow(deprecated)]
async fn get_neighboring_chat_media(
&self,
account_id: u32,
@@ -1874,7 +1870,7 @@ impl CommandApi {
.context("path conversion to string failed")
}
/// save a sticker to a collection/folder in the account's sticker folder
/// Saves a sticker to a collection/folder in the account's sticker folder.
async fn misc_save_sticker(
&self,
account_id: u32,
@@ -2053,23 +2049,6 @@ impl CommandApi {
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
}
// send the chat's current set draft
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
let mut draft = draft;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
.await?
.to_u32();
Ok(msg_id)
} else {
Err(anyhow!(
"chat with id {} doesn't have draft message",
chat_id
))
}
}
}
// Helper functions (to prevent code duplication)

View File

@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(tag = "kind")]
pub enum Account {
#[serde(rename_all = "camelCase")]
Configured {

View File

@@ -167,10 +167,11 @@ impl BasicChat {
}
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
pub enum MuteDuration {
NotMuted,
Forever,
Until(i64),
Until { duration: i64 },
}
impl MuteDuration {
@@ -178,13 +179,13 @@ impl MuteDuration {
match self {
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
MuteDuration::Until(n) => {
if n <= 0 {
MuteDuration::Until { duration } => {
if duration <= 0 {
bail!("failed to read mute duration")
}
Ok(SystemTime::now()
.checked_add(Duration::from_secs(n as u64))
.checked_add(Duration::from_secs(duration as u64))
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
}
}

View File

@@ -8,14 +8,17 @@ use deltachat::{
chatlist::Chatlist,
};
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::message::MessageViewtype;
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
pub struct ChatListEntry(pub u32, pub u32);
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(tag = "kind")]
pub enum ChatListItemFetchResult {
#[serde(rename_all = "camelCase")]
ChatListItem {

View File

@@ -22,7 +22,7 @@ impl From<CoreEvent> for Event {
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(tag = "kind")]
pub enum EventType {
/// The library-user may write an informational string to the log.
///

View File

@@ -19,7 +19,7 @@ use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "variant")]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },
@@ -318,7 +318,6 @@ pub enum DownloadState {
Done,
Available,
Failure,
Undecipherable,
InProgress,
}
@@ -328,7 +327,6 @@ impl From<download::DownloadState> for DownloadState {
download::DownloadState::Done => DownloadState::Done,
download::DownloadState::Available => DownloadState::Available,
download::DownloadState::Failure => DownloadState::Failure,
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
download::DownloadState::InProgress => DownloadState::InProgress,
}
}

View File

@@ -4,7 +4,7 @@ use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
#[serde(tag = "kind")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,

View File

@@ -13,7 +13,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let writable = true;
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
@@ -54,7 +55,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn test_batch_set_config() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let writable = true;
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();

View File

@@ -19,7 +19,8 @@ async fn main() -> Result<(), std::io::Error> {
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
.unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`.");
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
let state = CommandApi::new(accounts);
let app = Router::new()

View File

@@ -35,7 +35,7 @@ async function run() {
const accounts = await client.rpc.getAllAccounts();
console.log("accounts loaded", accounts);
for (const account of accounts) {
if (account.type === "Configured") {
if (account.kind === "Configured") {
write(
$head,
`<a href="#" onclick="selectDeltaAccount(${account.id})">
@@ -57,7 +57,7 @@ async function run() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT;
const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.type !== "Configured") {
if (info.kind !== "Configured") {
return write($main, "Account is not configured");
}
write($main, `<h2>${info.addr!}</h2>`);
@@ -81,8 +81,7 @@ async function run() {
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
if (message.variant === "message")
write($main, `<p>${message.text}</p>`);
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
else write($main, `<p>loading error: ${message.error}</p>`);
}
}
@@ -93,9 +92,9 @@ async function run() {
$side,
`
<p class="message">
[<strong>${event.type}</strong> on account ${accountId}]<br>
[<strong>${event.kind}</strong> on account ${accountId}]<br>
<em>f1:</em> ${JSON.stringify(
Object.assign({}, event, { type: undefined })
Object.assign({}, event, { kind: undefined })
)}
</p>`
);

View File

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

View File

@@ -6,22 +6,22 @@ import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
[Property in EventType["type"]]: (
[Property in EventType["kind"]]: (
accountId: number,
event: Extract<EventType, { type: Property }>
event: Extract<EventType, { kind: Property }>
) => void;
};
type ContextEvents = { ALL: (event: EventType) => void } & {
[Property in EventType["type"]]: (
event: Extract<EventType, { type: Property }>
[Property in EventType["kind"]]: (
event: Extract<EventType, { kind: Property }>
) => void;
};
export type DcEvent = EventType;
export type DcEventType<T extends EventType["type"]> = Extract<
export type DcEventType<T extends EventType["kind"]> = Extract<
EventType,
{ type: T }
{ kind: T }
>;
export class BaseDeltaChat<
@@ -46,12 +46,12 @@ export class BaseDeltaChat<
while (true) {
const event = await this.rpc.getNextEvent();
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event);
this.emit(event.event.kind, event.contextId, event.event);
this.emit("ALL", event.contextId, event.event);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
event.event.kind,
//@ts-ignore
event.event as any
);

View File

@@ -29,8 +29,8 @@ describe("online tests", function () {
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
dc.on("ALL", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);
dc.on("ALL", (contextId, { kind }) => {
if (kind !== "Info") console.log(contextId, kind);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
@@ -148,7 +148,7 @@ describe("online tests", function () {
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
// Check if answer arrives at A and if it is encrypted
// Check if answer arives at A and if it is encrypted
await eventPromise2;
const messageId = (
@@ -177,12 +177,12 @@ describe("online tests", function () {
});
});
async function waitForEvent<T extends DcEvent["type"]>(
async function waitForEvent<T extends DcEvent["kind"]>(
dc: DeltaChat,
eventType: T,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<Extract<DcEvent, { type: T }>> {
): Promise<Extract<DcEvent, { kind: T }>> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")),

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.126.1"
version = "1.120.0"
license = "MPL-2.0"
edition = "2021"

View File

@@ -18,6 +18,7 @@ use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
@@ -138,7 +139,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
/* import a directory */
let dir_name = std::path::Path::new(&real_spec);
let dir = fs::read_dir(dir_name).await;
if let Ok(mut dir) = dir {
if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return false;
} else {
let mut dir = dir.unwrap();
while let Ok(Some(entry)) = dir.next_entry().await {
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
@@ -150,9 +155,6 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
}
}
} else {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
return false;
}
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
@@ -186,7 +188,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
DownloadState::Available => " [⬇ Download available]",
DownloadState::InProgress => " [⬇ Download in progress...]",
DownloadState::Failure => " [⬇ Download failed]",
DownloadState::Undecipherable => " [⬇ Decryption failed]",
};
let temp2 = timestamp_to_str(msg.get_timestamp());
@@ -210,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.is_info() {
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
@@ -395,8 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unpin <chat-id>\n\
mute <chat-id> [<seconds>]\n\
unmute <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
accept <chat-id>\n\
decline <chat-id>\n\
@@ -805,30 +814,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"chatinfo" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
println!("Memberlist:");
log_contactlist(&context, &contacts).await?;
println!("{} contacts", contacts.len());
let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
if !similar_chats.is_empty() {
println!("Similar chats: ");
for (similar_chat_id, metric) in similar_chats {
let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
println!(
"{} (#{}) {:.1}",
similar_chat.name,
similar_chat_id,
100.0 * metric
);
}
}
println!(
"Location streaming: {}",
"{} contacts\nLocation streaming: {}",
contacts.len(),
location::is_sending_locations_to_chat(
&context,
Some(sel_chat.as_ref().unwrap().get_id())
@@ -893,7 +887,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
if continue_streaming {
println!("Success, streaming should be continued.");
} else {
@@ -1071,20 +1065,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
};
chat::set_muted(&context, chat_id, duration).await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);

View File

@@ -37,14 +37,19 @@ $ tox --devenv env
$ . env/bin/activate
```
It is recommended to use IPython, because it supports using `await` directly
from the REPL.
```
$ python
>>> from deltachat_rpc_client import *
>>> rpc = Rpc()
>>> rpc.start()
>>> dc = DeltaChat(rpc)
>>> system_info = dc.get_system_info()
>>> system_info["level"]
'awesome'
>>> rpc.close()
$ pip install ipython
$ PATH="../target/debug:$PATH" ipython
...
In [1]: from deltachat_rpc_client import *
In [2]: rpc = Rpc()
In [3]: await rpc.start()
In [4]: dc = DeltaChat(rpc)
In [5]: system_info = await dc.get_system_info()
In [6]: system_info["level"]
Out[6]: 'awesome'
In [7]: await rpc.close()
```

View File

@@ -4,21 +4,23 @@
it will echo back any text send to it, it also will print to console all Delta Chat core events.
Pass --help to the CLI to see available options.
"""
import asyncio
from deltachat_rpc_client import events, run_bot_cli
hooks = events.HookCollection()
@hooks.on(events.RawEvent)
def log_event(event):
async def log_event(event):
print(event)
@hooks.on(events.NewMessage)
def echo(event):
async def echo(event):
snapshot = event.message_snapshot
snapshot.chat.send_text(snapshot.text)
await snapshot.chat.send_text(snapshot.text)
if __name__ == "__main__":
run_bot_cli(hooks)
asyncio.run(run_bot_cli(hooks))

View File

@@ -3,9 +3,9 @@
it will echo back any message that has non-empty text and also supports the /help command.
"""
import asyncio
import logging
import sys
from threading import Thread
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
@@ -13,62 +13,62 @@ hooks = events.HookCollection()
@hooks.on(events.RawEvent)
def log_event(event):
if event.type == EventType.INFO:
async def log_event(event):
if event.kind == EventType.INFO:
logging.info(event.msg)
elif event.type == EventType.WARNING:
elif event.kind == EventType.WARNING:
logging.warning(event.msg)
@hooks.on(events.RawEvent(EventType.ERROR))
def log_error(event):
async def log_error(event):
logging.error(event.msg)
@hooks.on(events.MemberListChanged)
def on_memberlist_changed(event):
async def on_memberlist_changed(event):
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
@hooks.on(events.GroupImageChanged)
def on_group_image_changed(event):
async def on_group_image_changed(event):
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
@hooks.on(events.GroupNameChanged)
def on_group_name_changed(event):
async def on_group_name_changed(event):
logging.info("group name changed, old name: %s", event.old_name)
@hooks.on(events.NewMessage(func=lambda e: not e.command))
def echo(event):
async def echo(event):
snapshot = event.message_snapshot
if snapshot.text or snapshot.file:
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
@hooks.on(events.NewMessage(command="/help"))
def help_command(event):
async def help_command(event):
snapshot = event.message_snapshot
snapshot.chat.send_text("Send me any message and I will echo it back")
await snapshot.chat.send_text("Send me any message and I will echo it back")
def main():
with Rpc() as rpc:
async def main():
async with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = deltachat.get_system_info()
system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
bot = Bot(account, hooks)
if not bot.is_configured():
configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
configure_thread.start()
bot.run_forever()
if not await bot.is_configured():
# Save a reference to avoid garbage collection of the task.
_configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
await bot.run_forever()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()
asyncio.run(main())

View File

@@ -2,44 +2,45 @@
"""
Example echo bot without using hooks
"""
import asyncio
import logging
import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
def main():
with Rpc() as rpc:
async def main():
async with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = deltachat.get_system_info()
system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
account.set_config("bot", "1")
if not account.is_configured():
await account.set_config("bot", "1")
if not await account.is_configured():
logging.info("Account is not configured, configuring")
account.set_config("addr", sys.argv[1])
account.set_config("mail_pw", sys.argv[2])
account.configure()
await account.set_config("addr", sys.argv[1])
await account.set_config("mail_pw", sys.argv[2])
await account.configure()
logging.info("Configured")
else:
logging.info("Account is already configured")
deltachat.start_io()
await deltachat.start_io()
def process_messages():
for message in account.get_next_messages():
snapshot = message.get_snapshot()
async def process_messages():
for message in await account.get_next_messages():
snapshot = await message.get_snapshot()
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
snapshot.chat.send_text(snapshot.text)
snapshot.message.mark_seen()
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()
# Process old messages.
process_messages()
await process_messages()
while True:
event = account.wait_for_event()
event = await account.wait_for_event()
if event["type"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["type"] == EventType.WARNING:
@@ -48,9 +49,9 @@ def main():
logging.error("%s", event["msg"])
elif event["type"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
process_messages()
await process_messages()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()
asyncio.run(main())

View File

@@ -5,8 +5,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface"
dependencies = [
"aiohttp"
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: POSIX :: Linux",
@@ -71,6 +75,3 @@ line-length = 120
[tool.isort]
profile = "black"
[tool.pytest.ini_options]
log_cli = true

View File

@@ -1,4 +1,4 @@
"""Delta Chat JSON-RPC high-level API"""
"""Delta Chat asynchronous high-level API"""
from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
from .chat import Chat

View File

@@ -1,7 +1,7 @@
import argparse
import asyncio
import re
import sys
from threading import Thread
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
if TYPE_CHECKING:
@@ -43,7 +43,7 @@ class AttrDict(dict):
super().__setattr__(attr, val)
def run_client_cli(
async def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs,
@@ -54,10 +54,10 @@ def run_client_cli(
"""
from .client import Client
_run_cli(Client, hooks, argv, **kwargs)
await _run_cli(Client, hooks, argv, **kwargs)
def run_bot_cli(
async def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs,
@@ -68,10 +68,10 @@ def run_bot_cli(
"""
from .client import Bot
_run_cli(Bot, hooks, argv, **kwargs)
await _run_cli(Bot, hooks, argv, **kwargs)
def _run_cli(
async def _run_cli(
client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
@@ -93,20 +93,20 @@ def _run_cli(
parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:])
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
deltachat = DeltaChat(rpc)
core_version = (deltachat.get_system_info()).deltachat_core_version
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
core_version = (await deltachat.get_system_info()).deltachat_core_version
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version)
if not client.is_configured():
if not await client.is_configured():
assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided"
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
configure_thread.start()
client.run_forever()
# Save a reference to avoid garbage collection of the task.
_configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
await client.run_forever()
def extract_addr(text: str) -> str:

View File

@@ -24,63 +24,63 @@ class Account:
def _rpc(self) -> "Rpc":
return self.manager.rpc
def wait_for_event(self) -> AttrDict:
async def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
return AttrDict(self._rpc.wait_for_event(self.id))
return AttrDict(await self._rpc.wait_for_event(self.id))
def remove(self) -> None:
async def remove(self) -> None:
"""Remove the account."""
self._rpc.remove_account(self.id)
await self._rpc.remove_account(self.id)
def start_io(self) -> None:
async def start_io(self) -> None:
"""Start the account I/O."""
self._rpc.start_io(self.id)
await self._rpc.start_io(self.id)
def stop_io(self) -> None:
async def stop_io(self) -> None:
"""Stop the account I/O."""
self._rpc.stop_io(self.id)
await self._rpc.stop_io(self.id)
def get_info(self) -> AttrDict:
async def get_info(self) -> AttrDict:
"""Return dictionary of this account configuration parameters."""
return AttrDict(self._rpc.get_info(self.id))
return AttrDict(await self._rpc.get_info(self.id))
def get_size(self) -> int:
async def get_size(self) -> int:
"""Get the combined filesize of an account in bytes."""
return self._rpc.get_account_file_size(self.id)
return await self._rpc.get_account_file_size(self.id)
def is_configured(self) -> bool:
async def is_configured(self) -> bool:
"""Return True if this account is configured."""
return self._rpc.is_configured(self.id)
return await self._rpc.is_configured(self.id)
def set_config(self, key: str, value: Optional[str] = None) -> None:
async def set_config(self, key: str, value: Optional[str] = None) -> None:
"""Set configuration value."""
self._rpc.set_config(self.id, key, value)
await self._rpc.set_config(self.id, key, value)
def get_config(self, key: str) -> Optional[str]:
async def get_config(self, key: str) -> Optional[str]:
"""Get configuration value."""
return self._rpc.get_config(self.id, key)
return await self._rpc.get_config(self.id, key)
def update_config(self, **kwargs) -> None:
async def update_config(self, **kwargs) -> None:
"""update config values."""
for key, value in kwargs.items():
self.set_config(key, value)
await self.set_config(key, value)
def set_avatar(self, img_path: Optional[str] = None) -> None:
async def set_avatar(self, img_path: Optional[str] = None) -> None:
"""Set self avatar.
Passing None will discard the currently set avatar.
"""
self.set_config("selfavatar", img_path)
await self.set_config("selfavatar", img_path)
def get_avatar(self) -> Optional[str]:
async def get_avatar(self) -> Optional[str]:
"""Get self avatar."""
return self.get_config("selfavatar")
return await self.get_config("selfavatar")
def configure(self) -> None:
async def configure(self) -> None:
"""Configure an account."""
self._rpc.configure(self.id)
await self._rpc.configure(self.id)
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
Calling this method will always result in the same
@@ -94,24 +94,24 @@ class Account:
if isinstance(obj, int):
obj = Contact(self, obj)
if isinstance(obj, Contact):
obj = obj.get_snapshot().address
return Contact(self, self._rpc.create_contact(self.id, obj, name))
obj = (await obj.get_snapshot()).address
return Contact(self, await self._rpc.create_contact(self.id, obj, name))
def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID."""
return Contact(self, contact_id)
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Check if an e-mail address belongs to a known and unblocked contact."""
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
def get_blocked_contacts(self) -> List[AttrDict]:
async def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts."""
contacts = self._rpc.get_blocked_contacts(self.id)
contacts = await self._rpc.get_blocked_contacts(self.id)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
def get_contacts(
async def get_contacts(
self,
query: Optional[str] = None,
with_self: bool = False,
@@ -133,9 +133,9 @@ class Account:
flags |= ContactFlag.ADD_SELF
if snapshot:
contacts = self._rpc.get_contacts(self.id, flags, query)
contacts = await self._rpc.get_contacts(self.id, flags, query)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
contacts = self._rpc.get_contact_ids(self.id, flags, query)
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
return [Contact(self, contact_id) for contact_id in contacts]
@property
@@ -143,7 +143,7 @@ class Account:
"""This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
def get_chatlist(
async def get_chatlist(
self,
query: Optional[str] = None,
contact: Optional[Contact] = None,
@@ -175,29 +175,29 @@ class Account:
if alldone_hint:
flags |= ChatlistFlag.ADD_ALLDONE_HINT
entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
if not snapshot:
return [Chat(self, entry) for entry in entries]
items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
chats = []
for item in items.values():
item["chat"] = Chat(self, item["id"])
chats.append(AttrDict(item))
return chats
def create_group(self, name: str, protect: bool = False) -> Chat:
async def create_group(self, name: str, protect: bool = False) -> Chat:
"""Create a new group chat.
After creation, the group has only self-contact as member and is in unpromoted state.
"""
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID."""
return Chat(self, chat_id)
def secure_join(self, qrdata: str) -> Chat:
async def secure_join(self, qrdata: str) -> Chat:
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device.
@@ -208,62 +208,62 @@ class Account:
:param qrdata: The text of the scanned QR code.
"""
return Chat(self, self._rpc.secure_join(self.id, qrdata))
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
def get_qr_code(self) -> Tuple[str, str]:
async def get_qr_code(self) -> Tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data.
this data needs to be transferred to another Delta Chat account
in a second channel, typically used by mobiles with QRcode-show + scan UX.
"""
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
def get_message_by_id(self, msg_id: int) -> Message:
"""Return the Message instance with the given ID."""
return Message(self, msg_id)
def mark_seen_messages(self, messages: List[Message]) -> None:
async def mark_seen_messages(self, messages: List[Message]) -> None:
"""Mark the given set of messages as seen."""
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
def delete_messages(self, messages: List[Message]) -> None:
async def delete_messages(self, messages: List[Message]) -> None:
"""Delete messages (local and remote)."""
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
def get_fresh_messages(self) -> List[Message]:
async def get_fresh_messages(self) -> List[Message]:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
to process oldest messages first.
"""
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def get_next_messages(self) -> List[Message]:
async def get_next_messages(self) -> List[Message]:
"""Return a list of next messages."""
next_msg_ids = self._rpc.get_next_msgs(self.id)
next_msg_ids = await self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def wait_next_messages(self) -> List[Message]:
async def wait_next_messages(self) -> List[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = self._rpc.wait_next_msgs(self.id)
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning,
stacklevel=2,
)
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def export_backup(self, path, passphrase: str = "") -> None:
async def export_backup(self, path, passphrase: str = "") -> None:
"""Export backup."""
self._rpc.export_backup(self.id, str(path), passphrase)
await self._rpc.export_backup(self.id, str(path), passphrase)
def import_backup(self, path, passphrase: str = "") -> None:
async def import_backup(self, path, passphrase: str = "") -> None:
"""Import backup."""
self._rpc.import_backup(self.id, str(path), passphrase)
await self._rpc.import_backup(self.id, str(path), passphrase)

View File

@@ -25,7 +25,7 @@ class Chat:
def _rpc(self) -> "Rpc":
return self.account._rpc
def delete(self) -> None:
async def delete(self) -> None:
"""Delete this chat and all its messages.
Note:
@@ -33,83 +33,83 @@ class Chat:
- does not delete messages on server
- the chat or contact is not blocked, new message will arrive
"""
self._rpc.delete_chat(self.account.id, self.id)
await self._rpc.delete_chat(self.account.id, self.id)
def block(self) -> None:
async def block(self) -> None:
"""Block this chat."""
self._rpc.block_chat(self.account.id, self.id)
await self._rpc.block_chat(self.account.id, self.id)
def accept(self) -> None:
async def accept(self) -> None:
"""Accept this contact request chat."""
self._rpc.accept_chat(self.account.id, self.id)
await self._rpc.accept_chat(self.account.id, self.id)
def leave(self) -> None:
async def leave(self) -> None:
"""Leave this chat."""
self._rpc.leave_group(self.account.id, self.id)
await self._rpc.leave_group(self.account.id, self.id)
def mute(self, duration: Optional[int] = None) -> None:
async def mute(self, duration: Optional[int] = None) -> None:
"""Mute this chat, if a duration is not provided the chat is muted forever.
:param duration: mute duration from now in seconds. Must be greater than zero.
"""
if duration is not None:
assert duration > 0, "Invalid duration"
dur: Union[str, dict] = {"Until": duration}
dur: dict = {"kind": "Until", "duration": duration}
else:
dur = "Forever"
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
dur = {"kind": "Forever"}
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
def unmute(self) -> None:
async def unmute(self) -> None:
"""Unmute this chat."""
self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
await self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
def pin(self) -> None:
async def pin(self) -> None:
"""Pin this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
def unpin(self) -> None:
async def unpin(self) -> None:
"""Unpin this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
def archive(self) -> None:
async def archive(self) -> None:
"""Archive this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
def unarchive(self) -> None:
async def unarchive(self) -> None:
"""Unarchive this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
def set_name(self, name: str) -> None:
async def set_name(self, name: str) -> None:
"""Set name of this chat."""
self._rpc.set_chat_name(self.account.id, self.id, name)
await self._rpc.set_chat_name(self.account.id, self.id, name)
def set_ephemeral_timer(self, timer: int) -> None:
async def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat."""
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:
async def get_encryption_info(self) -> str:
"""Return encryption info for this chat."""
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
def get_qr_code(self) -> Tuple[str, str]:
async def get_qr_code(self) -> Tuple[str, str]:
"""Get Join-Group QR code text and SVG data."""
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
def get_basic_snapshot(self) -> AttrDict:
async def get_basic_snapshot(self) -> AttrDict:
"""Get a chat snapshot with basic info about this chat."""
info = self._rpc.get_basic_chat_info(self.account.id, self.id)
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
return AttrDict(chat=self, **info)
def get_full_snapshot(self) -> AttrDict:
async def get_full_snapshot(self) -> AttrDict:
"""Get a full snapshot of this chat."""
info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
return AttrDict(chat=self, **info)
def can_send(self) -> bool:
async def can_send(self) -> bool:
"""Return true if messages can be sent to the chat."""
return self._rpc.can_send(self.account.id, self.id)
return await self._rpc.can_send(self.account.id, self.id)
def send_message(
async def send_message(
self,
text: Optional[str] = None,
html: Optional[str] = None,
@@ -132,30 +132,30 @@ class Chat:
"overrideSenderName": override_sender_name,
"quotedMessageId": quoted_msg,
}
msg_id = self._rpc.send_msg(self.account.id, self.id, draft)
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
return Message(self.account, msg_id)
def send_text(self, text: str) -> Message:
async def send_text(self, text: str) -> Message:
"""Send a text message and return the resulting Message instance."""
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
return Message(self.account, msg_id)
def send_videochat_invitation(self) -> Message:
async def send_videochat_invitation(self) -> Message:
"""Send a videochat invitation and return the resulting Message instance."""
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
return Message(self.account, msg_id)
def send_sticker(self, path: str) -> Message:
async def send_sticker(self, path: str) -> Message:
"""Send an sticker and return the resulting Message instance."""
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
def forward_messages(self, messages: List[Message]) -> None:
async def forward_messages(self, messages: List[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
def set_draft(
async def set_draft(
self,
text: Optional[str] = None,
file: Optional[str] = None,
@@ -164,15 +164,15 @@ class Chat:
"""Set draft message."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
def remove_draft(self) -> None:
async def remove_draft(self) -> None:
"""Remove draft message."""
self._rpc.remove_draft(self.account.id, self.id)
await self._rpc.remove_draft(self.account.id, self.id)
def get_draft(self) -> Optional[AttrDict]:
async def get_draft(self) -> Optional[AttrDict]:
"""Get draft message."""
snapshot = self._rpc.get_draft(self.account.id, self.id)
snapshot = await self._rpc.get_draft(self.account.id, self.id)
if not snapshot:
return None
snapshot = AttrDict(snapshot)
@@ -181,61 +181,61 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
"""get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
def get_fresh_message_count(self) -> int:
async def get_fresh_message_count(self) -> int:
"""Get number of fresh messages in this chat"""
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
def mark_noticed(self) -> None:
async def mark_noticed(self) -> None:
"""Mark all messages in this chat as noticed."""
self._rpc.marknoticed_chat(self.account.id, self.id)
await self._rpc.marknoticed_chat(self.account.id, self.id)
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Add contacts to this group."""
for cnt in contact:
if isinstance(cnt, str):
contact_id = self.account.create_contact(cnt).id
contact_id = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group."""
for cnt in contact:
if isinstance(cnt, str):
contact_id = self.account.create_contact(cnt).id
contact_id = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
def get_contacts(self) -> List[Contact]:
async def get_contacts(self) -> List[Contact]:
"""Get the contacts belonging to this chat.
For single/direct chats self-address is not included.
"""
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts]
def set_image(self, path: str) -> None:
async def set_image(self, path: str) -> None:
"""Set profile image of this chat.
:param path: Full path of the image to use as the group image.
"""
self._rpc.set_chat_profile_image(self.account.id, self.id, path)
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
def remove_image(self) -> None:
async def remove_image(self) -> None:
"""Remove profile image of this chat."""
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
def get_locations(
async def get_locations(
self,
contact: Optional[Contact] = None,
timestamp_from: Optional["datetime"] = None,
@@ -246,7 +246,7 @@ class Chat:
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
contact_id = contact.id if contact else 0
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
locations = []
contacts: Dict[int, Contact] = {}
for loc in result:

View File

@@ -1,8 +1,10 @@
"""Event loop implementations offering high level event handling/hooking."""
import inspect
import logging
from typing import (
TYPE_CHECKING,
Callable,
Coroutine,
Dict,
Iterable,
Optional,
@@ -76,22 +78,22 @@ class Client:
)
self._hooks.get(type(event), set()).remove((hook, event))
def is_configured(self) -> bool:
return self.account.is_configured()
async def is_configured(self) -> bool:
return await self.account.is_configured()
def configure(self, email: str, password: str, **kwargs) -> None:
self.account.set_config("addr", email)
self.account.set_config("mail_pw", password)
async def configure(self, email: str, password: str, **kwargs) -> None:
await self.account.set_config("addr", email)
await self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
self.account.set_config(key, value)
self.account.configure()
await self.account.set_config(key, value)
await self.account.configure()
self.logger.debug("Account configured")
def run_forever(self) -> None:
async def run_forever(self) -> None:
"""Process events forever."""
self.run_until(lambda _: False)
await self.run_until(lambda _: False)
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
"""Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the
@@ -99,37 +101,39 @@ class Client:
evaluates to True.
"""
self.logger.debug("Listening to incoming events...")
if self.is_configured():
self.account.start_io()
self._process_messages() # Process old messages.
if await self.is_configured():
await self.account.start_io()
await self._process_messages() # Process old messages.
while True:
event = self.account.wait_for_event()
event["type"] = EventType(event.type)
event = await self.account.wait_for_event()
event["kind"] = EventType(event.kind)
event["account"] = self.account
self._on_event(event)
if event.type == EventType.INCOMING_MSG:
self._process_messages()
await self._on_event(event)
if event.kind == EventType.INCOMING_MSG:
await self._process_messages()
stop = func(event)
if inspect.isawaitable(stop):
stop = await stop
if stop:
return event
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
for hook, evfilter in self._hooks.get(filter_type, []):
if evfilter.filter(event):
if await evfilter.filter(event):
try:
hook(event)
await hook(event)
except Exception as ex:
self.logger.exception(ex)
def _parse_command(self, event: AttrDict) -> None:
async def _parse_command(self, event: AttrDict) -> None:
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
parts = event.message_snapshot.text.split(maxsplit=1)
payload = parts[1] if len(parts) > 1 else ""
cmd = parts.pop(0)
if "@" in cmd:
suffix = "@" + self.account.self_contact.get_snapshot().address
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
if cmd.endswith(suffix):
cmd = cmd[: -len(suffix)]
else:
@@ -149,32 +153,32 @@ class Client:
event["command"], event["payload"] = cmd, payload
def _on_new_msg(self, snapshot: AttrDict) -> None:
async def _on_new_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(command="", payload="", message_snapshot=snapshot)
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
self._parse_command(event)
self._on_event(event, NewMessage)
await self._parse_command(event)
await self._on_event(event, NewMessage)
def _handle_info_msg(self, snapshot: AttrDict) -> None:
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(message_snapshot=snapshot)
img_changed = parse_system_image_changed(snapshot.text)
if img_changed:
_, event["image_deleted"] = img_changed
self._on_event(event, GroupImageChanged)
await self._on_event(event, GroupImageChanged)
return
title_changed = parse_system_title_changed(snapshot.text)
if title_changed:
_, event["old_name"] = title_changed
self._on_event(event, GroupNameChanged)
await self._on_event(event, GroupNameChanged)
return
members_changed = parse_system_add_remove(snapshot.text)
if members_changed:
action, event["member"], _ = members_changed
event["member_added"] = action == "added"
self._on_event(event, MemberListChanged)
await self._on_event(event, MemberListChanged)
return
self.logger.warning(
@@ -183,20 +187,20 @@ class Client:
snapshot.text,
)
def _process_messages(self) -> None:
async def _process_messages(self) -> None:
if self._should_process_messages:
for message in self.account.get_next_messages():
snapshot = message.get_snapshot()
for message in await self.account.get_next_messages():
snapshot = await message.get_snapshot()
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
self._on_new_msg(snapshot)
await self._on_new_msg(snapshot)
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
self._handle_info_msg(snapshot)
snapshot.message.mark_seen()
await self._handle_info_msg(snapshot)
await snapshot.message.mark_seen()
class Bot(Client):
"""Simple bot implementation that listent to events of a single account."""
def configure(self, email: str, password: str, **kwargs) -> None:
async def configure(self, email: str, password: str, **kwargs) -> None:
kwargs.setdefault("bot", "1")
super().configure(email, password, **kwargs)
await super().configure(email, password, **kwargs)

View File

@@ -24,39 +24,39 @@ class Contact:
def _rpc(self) -> "Rpc":
return self.account._rpc
def block(self) -> None:
async def block(self) -> None:
"""Block contact."""
self._rpc.block_contact(self.account.id, self.id)
await self._rpc.block_contact(self.account.id, self.id)
def unblock(self) -> None:
async def unblock(self) -> None:
"""Unblock contact."""
self._rpc.unblock_contact(self.account.id, self.id)
await self._rpc.unblock_contact(self.account.id, self.id)
def delete(self) -> None:
async def delete(self) -> None:
"""Delete contact."""
self._rpc.delete_contact(self.account.id, self.id)
await self._rpc.delete_contact(self.account.id, self.id)
def set_name(self, name: str) -> None:
async def set_name(self, name: str) -> None:
"""Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name)
await self._rpc.change_contact_name(self.account.id, self.id, name)
def get_encryption_info(self) -> str:
async def get_encryption_info(self) -> str:
"""Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact.
"""
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
def get_snapshot(self) -> AttrDict:
async def get_snapshot(self) -> AttrDict:
"""Return a dictionary with a snapshot of all contact properties."""
snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id))
snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
snapshot["contact"] = self
return snapshot
def create_chat(self) -> "Chat":
async def create_chat(self) -> "Chat":
"""Create or get an existing 1:1 chat for this contact."""
from .chat import Chat
return Chat(
self.account,
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
)

View File

@@ -16,34 +16,34 @@ class DeltaChat:
def __init__(self, rpc: "Rpc") -> None:
self.rpc = rpc
def add_account(self) -> Account:
async def add_account(self) -> Account:
"""Create a new account database."""
account_id = self.rpc.add_account()
account_id = await self.rpc.add_account()
return Account(self, account_id)
def get_all_accounts(self) -> List[Account]:
async def get_all_accounts(self) -> List[Account]:
"""Return a list of all available accounts."""
account_ids = self.rpc.get_all_account_ids()
account_ids = await self.rpc.get_all_account_ids()
return [Account(self, account_id) for account_id in account_ids]
def start_io(self) -> None:
async def start_io(self) -> None:
"""Start the I/O of all accounts."""
self.rpc.start_io_for_all_accounts()
await self.rpc.start_io_for_all_accounts()
def stop_io(self) -> None:
async def stop_io(self) -> None:
"""Stop the I/O of all accounts."""
self.rpc.stop_io_for_all_accounts()
await self.rpc.stop_io_for_all_accounts()
def maybe_network(self) -> None:
async def maybe_network(self) -> None:
"""Indicate that the network likely has come back or just that the network
conditions might have changed.
"""
self.rpc.maybe_network()
await self.rpc.maybe_network()
def get_system_info(self) -> AttrDict:
async def get_system_info(self) -> AttrDict:
"""Get information about the Delta Chat core in this system."""
return AttrDict(self.rpc.get_system_info())
return AttrDict(await self.rpc.get_system_info())
def set_translations(self, translations: Dict[str, str]) -> None:
async def set_translations(self, translations: Dict[str, str]) -> None:
"""Set stock translation strings."""
self.rpc.set_stock_strings(translations)
await self.rpc.set_stock_strings(translations)

View File

@@ -1,4 +1,5 @@
"""High-level classes for event processing and filtering."""
import inspect
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
@@ -23,7 +24,7 @@ def _tuple_of(obj, type_: type) -> tuple:
class EventFilter(ABC):
"""The base event filter.
:param func: A Callable function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -42,13 +43,16 @@ class EventFilter(ABC):
def __ne__(self, other):
return not self == other
def _call_func(self, event) -> bool:
async def _call_func(self, event) -> bool:
if not self.func:
return True
return self.func(event)
res = self.func(event)
if inspect.isawaitable(res):
return await res
return res
@abstractmethod
def filter(self, event):
async def filter(self, event):
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
@@ -58,7 +62,7 @@ class RawEvent(EventFilter):
"""Matches raw core events.
:param types: The types of event to match.
:param func: A Callable function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -78,10 +82,10 @@ class RawEvent(EventFilter):
return (self.types, self.func) == (other.types, other.func)
return False
def filter(self, event: "AttrDict") -> bool:
if self.types and event.type not in self.types:
async def filter(self, event: "AttrDict") -> bool:
if self.types and event.kind not in self.types:
return False
return self._call_func(event)
return await self._call_func(event)
class NewMessage(EventFilter):
@@ -100,7 +104,7 @@ class NewMessage(EventFilter):
:param is_info: If set to True only match info/system messages, if set to False
only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched.
:param func: A Callable function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -155,7 +159,7 @@ class NewMessage(EventFilter):
)
return False
def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: "AttrDict") -> bool:
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
@@ -164,9 +168,11 @@ class NewMessage(EventFilter):
return False
if self.pattern:
match = self.pattern(event.message_snapshot.text)
if inspect.isawaitable(match):
match = await match
if not match:
return False
return super()._call_func(event)
return await super()._call_func(event)
class MemberListChanged(EventFilter):
@@ -178,7 +184,7 @@ class MemberListChanged(EventFilter):
:param added: If set to True only match if a member was added, if set to False
only match if a member was removed. If omitted both, member additions
and removals, will be matched.
:param func: A Callable function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -195,10 +201,10 @@ class MemberListChanged(EventFilter):
return (self.added, self.func) == (other.added, other.func)
return False
def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: "AttrDict") -> bool:
if self.added is not None and self.added != event.member_added:
return False
return self._call_func(event)
return await self._call_func(event)
class GroupImageChanged(EventFilter):
@@ -210,7 +216,7 @@ class GroupImageChanged(EventFilter):
:param deleted: If set to True only match if the image was deleted, if set to False
only match if a new image was set. If omitted both, image changes and
removals, will be matched.
:param func: A Callable function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -227,10 +233,10 @@ class GroupImageChanged(EventFilter):
return (self.deleted, self.func) == (other.deleted, other.func)
return False
def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: "AttrDict") -> bool:
if self.deleted is not None and self.deleted != event.image_deleted:
return False
return self._call_func(event)
return await self._call_func(event)
class GroupNameChanged(EventFilter):
@@ -239,7 +245,7 @@ class GroupNameChanged(EventFilter):
Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots.
:param func: A Callable function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -252,8 +258,8 @@ class GroupNameChanged(EventFilter):
return self.func == other.func
return False
def filter(self, event: "AttrDict") -> bool:
return self._call_func(event)
async def filter(self, event: "AttrDict") -> bool:
return await self._call_func(event)
class HookCollection:

View File

@@ -21,39 +21,39 @@ class Message:
def _rpc(self) -> "Rpc":
return self.account._rpc
def send_reaction(self, *reaction: str):
async def send_reaction(self, *reaction: str):
"""Send a reaction to this message."""
self._rpc.send_reaction(self.account.id, self.id, reaction)
await self._rpc.send_reaction(self.account.id, self.id, reaction)
def get_snapshot(self) -> AttrDict:
async def get_snapshot(self) -> AttrDict:
"""Get a snapshot with the properties of this message."""
from .chat import Chat
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = self
return snapshot
def get_reactions(self) -> Optional[AttrDict]:
async def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions."""
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
if reactions:
return AttrDict(reactions)
return None
def mark_seen(self) -> None:
async def mark_seen(self) -> None:
"""Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id])
await self._rpc.markseen_msgs(self.account.id, [self.id])
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
"""Send a webxdc status update. This message must be a webxdc."""
if not isinstance(update, str):
update = json.dumps(update)
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
def get_webxdc_info(self) -> dict:
return self._rpc.get_webxdc_info(self.account.id, self.id)
async def get_webxdc_info(self) -> dict:
return await self._rpc.get_webxdc_info(self.account.id, self.id)

View File

@@ -1,67 +1,70 @@
import asyncio
import json
import os
import urllib.request
from typing import AsyncGenerator, List, Optional
import pytest
import aiohttp
import pytest_asyncio
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from .rpc import Rpc
def get_temp_credentials() -> dict:
async def get_temp_credentials() -> dict:
url = os.getenv("DCC_NEW_TMP_EMAIL")
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
request = urllib.request.Request(url, method="POST")
with urllib.request.urlopen(request, timeout=60) as f:
return json.load(f)
# Replace default 5 minute timeout with a 1 minute timeout.
timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession() as session, session.post(url, timeout=timeout) as response:
return json.loads(await response.text())
class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
def get_unconfigured_account(self) -> Account:
return self.deltachat.add_account()
async def get_unconfigured_account(self) -> Account:
return await self.deltachat.add_account()
def get_unconfigured_bot(self) -> Bot:
return Bot(self.get_unconfigured_account())
async def get_unconfigured_bot(self) -> Bot:
return Bot(await self.get_unconfigured_account())
def new_preconfigured_account(self) -> Account:
async def new_preconfigured_account(self) -> Account:
"""Make a new account with configuration options set, but configuration not started."""
credentials = get_temp_credentials()
account = self.get_unconfigured_account()
account.set_config("addr", credentials["email"])
account.set_config("mail_pw", credentials["password"])
assert not account.is_configured()
credentials = await get_temp_credentials()
account = await self.get_unconfigured_account()
await account.set_config("addr", credentials["email"])
await account.set_config("mail_pw", credentials["password"])
assert not await account.is_configured()
return account
def new_configured_account(self) -> Account:
account = self.new_preconfigured_account()
account.configure()
assert account.is_configured()
async def new_configured_account(self) -> Account:
account = await self.new_preconfigured_account()
await account.configure()
assert await account.is_configured()
return account
def new_configured_bot(self) -> Bot:
credentials = get_temp_credentials()
bot = self.get_unconfigured_bot()
bot.configure(credentials["email"], credentials["password"])
async def new_configured_bot(self) -> Bot:
credentials = await get_temp_credentials()
bot = await self.get_unconfigured_bot()
await bot.configure(credentials["email"], credentials["password"])
return bot
def get_online_account(self) -> Account:
account = self.new_configured_account()
account.start_io()
async def get_online_account(self) -> Account:
account = await self.new_configured_account()
await account.start_io()
while True:
event = account.wait_for_event()
if event.type == EventType.IMAP_INBOX_IDLE:
event = await account.wait_for_event()
print(event)
if event.kind == EventType.IMAP_INBOX_IDLE:
break
return account
def get_online_accounts(self, num: int) -> List[Account]:
return [self.get_online_account() for _ in range(num)]
async def get_online_accounts(self, num: int) -> List[Account]:
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
def send_message(
async def send_message(
self,
to_account: Account,
from_account: Optional[Account] = None,
@@ -70,16 +73,16 @@ class ACFactory:
group: Optional[str] = None,
) -> Message:
if not from_account:
from_account = (self.get_online_accounts(1))[0]
to_contact = from_account.create_contact(to_account.get_config("addr"))
from_account = (await self.get_online_accounts(1))[0]
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
if group:
to_chat = from_account.create_group(group)
to_chat.add_contact(to_contact)
to_chat = await from_account.create_group(group)
await to_chat.add_contact(to_contact)
else:
to_chat = to_contact.create_chat()
return to_chat.send_message(text=text, file=file)
to_chat = await to_contact.create_chat()
return await to_chat.send_message(text=text, file=file)
def process_message(
async def process_message(
self,
to_client: Client,
from_account: Optional[Account] = None,
@@ -87,7 +90,7 @@ class ACFactory:
file: Optional[str] = None,
group: Optional[str] = None,
) -> AttrDict:
self.send_message(
await self.send_message(
to_account=to_client.account,
from_account=from_account,
text=text,
@@ -95,16 +98,16 @@ class ACFactory:
group=group,
)
return to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
return await to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
@pytest.fixture()
def rpc(tmp_path) -> AsyncGenerator:
@pytest_asyncio.fixture
async def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
async with rpc_server:
yield rpc_server
@pytest.fixture()
def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc))
@pytest_asyncio.fixture
async def acfactory(rpc) -> AsyncGenerator:
yield ACFactory(DeltaChat(rpc))

View File

@@ -1,10 +1,6 @@
import asyncio
import json
import logging
import os
import subprocess
import sys
from queue import Queue
from threading import Event, Thread
from typing import Any, Dict, Optional
@@ -14,7 +10,7 @@ class JsonRpcError(Exception):
class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to subprocess.Popen()"""
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
@@ -22,142 +18,95 @@ class Rpc:
}
self._kwargs = kwargs
self.process: subprocess.Popen
self.process: asyncio.subprocess.Process
self.id: int
self.event_queues: Dict[int, Queue]
# Map from request ID to `threading.Event`.
self.request_events: Dict[int, Event]
# Map from request ID to the result.
self.request_results: Dict[int, Any]
self.request_queue: Queue[Any]
self.event_queues: Dict[int, asyncio.Queue]
# Map from request ID to `asyncio.Future` returning the response.
self.request_events: Dict[int, asyncio.Future]
self.closing: bool
self.reader_thread: Thread
self.writer_thread: Thread
self.events_thread: Thread
self.reader_task: asyncio.Task
self.events_task: asyncio.Task
def start(self) -> None:
if sys.version_info >= (3, 11):
self.process = subprocess.Popen(
"deltachat-rpc-server",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# Prevent subprocess from capturing SIGINT.
process_group=0,
**self._kwargs,
)
else:
self.process = subprocess.Popen(
"deltachat-rpc-server",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# `process_group` is not supported before Python 3.11.
preexec_fn=os.setpgrp, # noqa: PLW1509
**self._kwargs,
)
async def start(self) -> None:
self.process = await asyncio.create_subprocess_exec(
"deltachat-rpc-server",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
**self._kwargs,
)
self.id = 0
self.event_queues = {}
self.request_events = {}
self.request_results = {}
self.request_queue = Queue()
self.closing = False
self.reader_thread = Thread(target=self.reader_loop)
self.reader_thread.start()
self.writer_thread = Thread(target=self.writer_loop)
self.writer_thread.start()
self.events_thread = Thread(target=self.events_loop)
self.events_thread.start()
self.reader_task = asyncio.create_task(self.reader_loop())
self.events_task = asyncio.create_task(self.events_loop())
def close(self) -> None:
async def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
self.stop_io_for_all_accounts()
self.events_thread.join()
self.process.stdin.close()
self.reader_thread.join()
self.request_queue.put(None)
self.writer_thread.join()
await self.stop_io_for_all_accounts()
await self.events_task
self.process.terminate()
await self.reader_task
def __enter__(self):
self.start()
async def __aenter__(self):
await self.start()
return self
def __exit__(self, _exc_type, _exc, _tb):
self.close()
async def __aexit__(self, _exc_type, _exc, _tb):
await self.close()
def reader_loop(self) -> None:
try:
while True:
line = self.process.stdout.readline()
if not line: # EOF
break
response = json.loads(line)
if "id" in response:
response_id = response["id"]
event = self.request_events.pop(response_id)
self.request_results[response_id] = response
event.set()
else:
logging.warning("Got a response without ID: %s", response)
except Exception:
# Log an exception if the reader loop dies.
logging.exception("Exception in the reader loop")
async def reader_loop(self) -> None:
while True:
line = await self.process.stdout.readline() # noqa
if not line: # EOF
break
response = json.loads(line)
if "id" in response:
fut = self.request_events.pop(response["id"])
fut.set_result(response)
else:
print(response)
def writer_loop(self) -> None:
"""Writer loop ensuring only a single thread writes requests."""
try:
while True:
request = self.request_queue.get()
if not request:
break
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data)
self.process.stdin.flush()
except Exception:
# Log an exception if the writer loop dies.
logging.exception("Exception in the writer loop")
def get_queue(self, account_id: int) -> Queue:
async def get_queue(self, account_id: int) -> asyncio.Queue:
if account_id not in self.event_queues:
self.event_queues[account_id] = Queue()
self.event_queues[account_id] = asyncio.Queue()
return self.event_queues[account_id]
def events_loop(self) -> None:
async def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
try:
while True:
if self.closing:
return
event = self.get_next_event()
account_id = event["contextId"]
queue = self.get_queue(account_id)
queue.put(event["event"])
except Exception:
# Log an exception if the event loop dies.
logging.exception("Exception in the event loop")
while True:
if self.closing:
return
event = await self.get_next_event()
account_id = event["contextId"]
queue = await self.get_queue(account_id)
await queue.put(event["event"])
def wait_for_event(self, account_id: int) -> Optional[dict]:
async def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
queue = self.get_queue(account_id)
return queue.get()
queue = await self.get_queue(account_id)
return await queue.get()
def __getattr__(self, attr: str):
def method(*args) -> Any:
async def method(*args, **kwargs) -> Any:
self.id += 1
request_id = self.id
assert not (args and kwargs), "Mixing positional and keyword arguments"
request = {
"jsonrpc": "2.0",
"method": attr,
"params": args,
"params": kwargs or args,
"id": self.id,
}
event = Event()
self.request_events[request_id] = event
self.request_queue.put(request)
event.wait()
response = self.request_results.pop(request_id)
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data) # noqa
loop = asyncio.get_running_loop()
fut = loop.create_future()
self.request_events[request_id] = fut
response = await fut
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:

View File

@@ -1,4 +1,6 @@
import concurrent.futures
import asyncio
import json
import subprocess
from unittest.mock import MagicMock
import pytest
@@ -6,26 +8,26 @@ from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
def test_system_info(rpc) -> None:
system_info = rpc.get_system_info()
@pytest.mark.asyncio()
async def test_system_info(rpc) -> None:
system_info = await rpc.get_system_info()
assert "arch" in system_info
assert "deltachat_core_version" in system_info
def test_sleep(rpc) -> None:
@pytest.mark.asyncio()
async def test_sleep(rpc) -> None:
"""Test that long-running task does not block short-running task from completion."""
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
sleep_5_future = executor.submit(rpc.sleep, 5.0)
sleep_3_future = executor.submit(rpc.sleep, 3.0)
done, pending = concurrent.futures.wait(
[sleep_5_future, sleep_3_future],
return_when=concurrent.futures.FIRST_COMPLETED,
)
assert sleep_3_future in done
assert sleep_5_future in pending
sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
assert sleep_3_task in done
assert sleep_5_task in pending
sleep_5_task.cancel()
def test_email_address_validity(rpc) -> None:
@pytest.mark.asyncio()
async def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
@@ -33,16 +35,17 @@ def test_email_address_validity(rpc) -> None:
invalid_addresses = ["email@", "example.com", "emai221"]
for addr in valid_addresses:
assert rpc.check_email_validity(addr)
assert await rpc.check_email_validity(addr)
for addr in invalid_addresses:
assert not rpc.check_email_validity(addr)
assert not await rpc.check_email_validity(addr)
def test_acfactory(acfactory) -> None:
account = acfactory.new_configured_account()
@pytest.mark.asyncio()
async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
while True:
event = account.wait_for_event()
if event.type == EventType.CONFIGURE_PROGRESS:
event = await account.wait_for_event()
if event.kind == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success
break
@@ -51,241 +54,248 @@ def test_acfactory(acfactory) -> None:
print("Successful configuration")
def test_configure_starttls(acfactory) -> None:
account = acfactory.new_preconfigured_account()
@pytest.mark.asyncio()
async def test_configure_starttls(acfactory) -> None:
account = await acfactory.new_preconfigured_account()
# Use STARTTLS
account.set_config("mail_security", "2")
account.set_config("send_security", "2")
account.configure()
assert account.is_configured()
await account.set_config("mail_security", "2")
await account.set_config("send_security", "2")
await account.configure()
assert await account.is_configured()
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_account(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
while True:
event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
bob.mark_seen_messages([message])
await bob.mark_seen_messages([message])
assert alice != bob
assert repr(alice)
assert alice.get_info().level
assert alice.get_size()
assert alice.is_configured()
assert not alice.get_avatar()
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
assert alice.get_contacts()
assert alice.get_contacts(snapshot=True)
assert (await alice.get_info()).level
assert await alice.get_size()
assert await alice.is_configured()
assert not await alice.get_avatar()
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
assert await alice.get_contacts()
assert await alice.get_contacts(snapshot=True)
assert alice.self_contact
assert alice.get_chatlist()
assert alice.get_chatlist(snapshot=True)
assert alice.get_qr_code()
assert alice.get_fresh_messages()
assert alice.get_next_messages()
assert await alice.get_chatlist()
assert await alice.get_chatlist(snapshot=True)
assert await alice.get_qr_code()
assert await alice.get_fresh_messages()
assert await alice.get_next_messages()
# Test sending empty message.
assert len(bob.wait_next_messages()) == 0
alice_chat_bob.send_text("")
messages = bob.wait_next_messages()
assert len(await bob.wait_next_messages()) == 0
await alice_chat_bob.send_text("")
messages = await bob.wait_next_messages()
assert len(messages) == 1
message = messages[0]
snapshot = message.get_snapshot()
snapshot = await message.get_snapshot()
assert snapshot.text == ""
bob.mark_seen_messages([message])
await bob.mark_seen_messages([message])
group = alice.create_group("test group")
group.add_contact(alice_contact_bob)
group_msg = group.send_message(text="hello")
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
group_msg = await group.send_message(text="hello")
assert group_msg == alice.get_message_by_id(group_msg.id)
assert group == alice.get_chat_by_id(group.id)
alice.delete_messages([group_msg])
await alice.delete_messages([group_msg])
alice.set_config("selfstatus", "test")
assert alice.get_config("selfstatus") == "test"
alice.update_config(selfstatus="test2")
assert alice.get_config("selfstatus") == "test2"
await alice.set_config("selfstatus", "test")
assert await alice.get_config("selfstatus") == "test"
await alice.update_config(selfstatus="test2")
assert await alice.get_config("selfstatus") == "test2"
assert not alice.get_blocked_contacts()
alice_contact_bob.block()
blocked_contacts = alice.get_blocked_contacts()
assert not await alice.get_blocked_contacts()
await alice_contact_bob.block()
blocked_contacts = await alice.get_blocked_contacts()
assert blocked_contacts
assert blocked_contacts[0].contact == alice_contact_bob
bob.remove()
alice.stop_io()
await bob.remove()
await alice.stop_io()
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_chat(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
while True:
event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
bob_chat_alice = bob.get_chat_by_id(chat_id)
assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob)
alice_chat_bob.delete()
assert not bob_chat_alice.can_send()
bob_chat_alice.accept()
assert bob_chat_alice.can_send()
bob_chat_alice.block()
bob_chat_alice = snapshot.sender.create_chat()
bob_chat_alice.mute()
bob_chat_alice.unmute()
bob_chat_alice.pin()
bob_chat_alice.unpin()
bob_chat_alice.archive()
bob_chat_alice.unarchive()
await alice_chat_bob.delete()
assert not await bob_chat_alice.can_send()
await bob_chat_alice.accept()
assert await bob_chat_alice.can_send()
await bob_chat_alice.block()
bob_chat_alice = await snapshot.sender.create_chat()
await bob_chat_alice.mute()
await bob_chat_alice.unmute()
await bob_chat_alice.pin()
await bob_chat_alice.unpin()
await bob_chat_alice.archive()
await bob_chat_alice.unarchive()
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
bob_chat_alice.set_name("test")
bob_chat_alice.set_ephemeral_timer(300)
bob_chat_alice.get_encryption_info()
await bob_chat_alice.set_name("test")
await bob_chat_alice.set_ephemeral_timer(300)
await bob_chat_alice.get_encryption_info()
group = alice.create_group("test group")
group.add_contact(alice_contact_bob)
group.get_qr_code()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
await group.get_qr_code()
snapshot = group.get_basic_snapshot()
snapshot = await group.get_basic_snapshot()
assert snapshot.name == "test group"
group.set_name("new name")
snapshot = group.get_full_snapshot()
await group.set_name("new name")
snapshot = await group.get_full_snapshot()
assert snapshot.name == "new name"
msg = group.send_message(text="hi")
assert (msg.get_snapshot()).text == "hi"
group.forward_messages([msg])
msg = await group.send_message(text="hi")
assert (await msg.get_snapshot()).text == "hi"
await group.forward_messages([msg])
group.set_draft(text="test draft")
draft = group.get_draft()
await group.set_draft(text="test draft")
draft = await group.get_draft()
assert draft.text == "test draft"
group.remove_draft()
assert not group.get_draft()
await group.remove_draft()
assert not await group.get_draft()
assert group.get_messages()
group.get_fresh_message_count()
group.mark_noticed()
assert group.get_contacts()
group.remove_contact(alice_chat_bob)
group.get_locations()
assert await group.get_messages()
await group.get_fresh_message_count()
await group.mark_noticed()
assert await group.get_contacts()
await group.remove_contact(alice_chat_bob)
await group.get_locations()
def test_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_contact(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob)
alice_contact_bob.block()
alice_contact_bob.unblock()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()
await alice_contact_bob.block()
await alice_contact_bob.unblock()
await alice_contact_bob.set_name("new name")
await alice_contact_bob.get_encryption_info()
snapshot = await alice_contact_bob.get_snapshot()
assert snapshot.address == bob_addr
alice_contact_bob.create_chat()
await alice_contact_bob.create_chat()
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_message(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
while True:
event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
assert not snapshot.is_bot
assert repr(message)
with pytest.raises(JsonRpcError): # chat is not accepted
snapshot.chat.send_text("hi")
snapshot.chat.accept()
snapshot.chat.send_text("hi")
await snapshot.chat.send_text("hi")
await snapshot.chat.accept()
await snapshot.chat.send_text("hi")
message.mark_seen()
message.send_reaction("😎")
reactions = message.get_reactions()
await message.mark_seen()
await message.send_reaction("😎")
reactions = await message.get_reactions()
assert reactions
snapshot = message.get_snapshot()
snapshot = await message.get_snapshot()
assert reactions == snapshot.reactions
def test_is_bot(acfactory) -> None:
@pytest.mark.asyncio()
async def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
# Alice becomes a bot.
alice.set_config("bot", "1")
alice_chat_bob.send_text("Hello!")
await alice.set_config("bot", "1")
await alice_chat_bob.send_text("Hello!")
while True:
event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot = await message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
break
def test_bot(acfactory) -> None:
@pytest.mark.asyncio()
async def test_bot(acfactory) -> None:
mock = MagicMock()
user = (acfactory.get_online_accounts(1))[0]
bot = acfactory.new_configured_bot()
bot2 = acfactory.new_configured_bot()
user = (await acfactory.get_online_accounts(1))[0]
bot = await acfactory.new_configured_bot()
bot2 = await acfactory.new_configured_bot()
assert bot.is_configured()
assert bot.account.get_config("bot") == "1"
assert await bot.is_configured()
assert await bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook)
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook)
@@ -297,52 +307,63 @@ def test_bot(acfactory) -> None:
hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help"))
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id)
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook)
mock.hook.reset_mock()
acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id)
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
@pytest.mark.asyncio()
async def test_wait_next_messages(acfactory) -> None:
alice = await acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
bot = acfactory.new_preconfigured_account()
bot.set_config("bot", "1")
bot.configure()
bot = await acfactory.new_preconfigured_account()
await bot.set_config("bot", "1")
await bot.configure()
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
assert not await bot.wait_next_messages()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# Bot starts waiting for messages.
next_messages_task = executor.submit(bot.wait_next_messages)
# Bot starts waiting for messages.
next_messages_task = asyncio.create_task(bot.wait_next_messages())
bot_addr = bot.get_config("addr")
alice_contact_bot = alice.create_contact(bot_addr, "Bob")
alice_chat_bot = alice_contact_bot.create_chat()
alice_chat_bot.send_text("Hello!")
bot_addr = await bot.get_config("addr")
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
alice_chat_bot = await alice_contact_bot.create_chat()
await alice_chat_bot.send_text("Hello!")
next_messages = next_messages_task.result()
assert len(next_messages) == 1
snapshot = next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"
next_messages = await next_messages_task
assert len(next_messages) == 1
snapshot = await next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"
def test_import_export(acfactory, tmp_path) -> None:
alice = acfactory.new_configured_account()
alice.export_backup(tmp_path)
@pytest.mark.asyncio()
async def test_import_export(acfactory, tmp_path) -> None:
alice = await acfactory.new_configured_account()
await alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
alice2 = await acfactory.get_unconfigured_account()
await alice2.import_backup(files[0])
assert await alice2.manager.get_system_info()
def test_openrpc_command_line() -> None:
"""Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
openrpc = json.loads(out)
assert "openrpc" in openrpc
assert "methods" in openrpc

View File

@@ -1,22 +1,24 @@
import pytest
from deltachat_rpc_client import EventType
def test_webxdc(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_webxdc(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
while True:
event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id)
break
webxdc_info = message.get_webxdc_info()
webxdc_info = await message.get_webxdc_info()
assert webxdc_info == {
"document": None,
"icon": "icon.png",
@@ -26,20 +28,20 @@ def test_webxdc(acfactory) -> None:
"summary": None,
}
status_updates = message.get_webxdc_status_updates()
status_updates = await message.get_webxdc_status_updates()
assert status_updates == []
bob_chat_alice.accept()
message.send_webxdc_status_update({"payload": 42}, "")
message.send_webxdc_status_update({"payload": "Second update"}, "description")
await bob_chat_alice.accept()
await message.send_webxdc_status_update({"payload": 42}, "")
await message.send_webxdc_status_update({"payload": "Second update"}, "description")
status_updates = message.get_webxdc_status_updates()
status_updates = await message.get_webxdc_status_updates()
assert status_updates == [
{"payload": 42, "serial": 1, "max_serial": 2},
{"payload": "Second update", "serial": 2, "max_serial": 2},
]
status_updates = message.get_webxdc_status_updates(1)
status_updates = await message.get_webxdc_status_updates(1)
assert status_updates == [
{"payload": "Second update", "serial": 2, "max_serial": 2},
]

View File

@@ -6,7 +6,7 @@ envlist =
[testenv]
commands =
pytest -n6 {posargs}
pytest {posargs}
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
@@ -14,8 +14,10 @@ passenv =
DCC_NEW_TMP_EMAIL
deps =
pytest
pytest-asyncio
pytest-timeout
pytest-xdist
aiohttp
aiodns
[testenv:lint]
skipsdist = True

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.126.1"
version = "1.120.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -21,7 +21,7 @@ serde_json = "1.0.99"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.29.1", features = ["io-std"] }
tokio-util = "0.7.8"
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
[features]
default = ["vendored"]

View File

@@ -32,3 +32,6 @@ languages other than Rust, for example:
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
Run `deltachat-rpc-server --version` to check the version of the server.
Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.

View File

@@ -10,6 +10,7 @@ use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use yerpc::RpcServer as _;
#[cfg(target_family = "unix")]
use tokio::signal::unix as signal_unix;
@@ -39,6 +40,12 @@ async fn main_impl() -> Result<()> {
}
eprintln!("{}", &*DC_VERSION_STR);
return Ok(());
} else if first_arg.to_str() == Some("--openrpc") {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
}
println!("{}", CommandApi::openrpc_specification()?);
return Ok(());
} else {
return Err(anyhow!("Unrecognized option {:?}", first_arg));
}
@@ -56,7 +63,8 @@ async fn main_impl() -> Result<()> {
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
let accounts = Accounts::new(PathBuf::from(&path)).await?;
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
log::info!("Creating JSON-RPC API.");
let accounts = Arc::new(RwLock::new(accounts));

View File

@@ -3,6 +3,11 @@ unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
# Exponential CPU time usage for TLS certificate processing in webpki.
# It is only used for backup transfer, so does not affect IMAP and SMTP connections.
# Waiting for `iroh` to update dependencies.
"RUSTSEC-2023-0052",
]
[bans]
@@ -24,12 +29,11 @@ skip = [
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "idna", version = "<0.3" },
{ name = "indexmap", version = "<2.0.0" },
{ name = "linux-raw-sys", version = "0.3.8" },
{ name = "num-derive", version = "0.3.3" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
@@ -37,8 +41,8 @@ skip = [
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rustix", version = "0.37.21" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
@@ -54,8 +58,8 @@ skip = [
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
{ name = "winreg", version = "0.10.1" },

View File

@@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre
#### Upsides:
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)

View File

@@ -1,100 +0,0 @@
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::message::Message;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use tempfile::tempdir;
fn cb(event: EventType) {
match event {
EventType::ConfigureProgress { progress, .. } => {
log::info!("progress: {}", progress);
}
EventType::Info(msg) => {
log::info!("{}", msg);
}
EventType::Warning(msg) => {
log::warn!("{}", msg);
}
EventType::Error(msg) => {
log::error!("{}", msg);
}
event => {
log::info!("{:?}", event);
}
}
}
/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
#[tokio::main]
async fn main() {
pretty_env_logger::try_init_timed().ok();
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
log::info!("creating database {:?}", dbfile);
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
.await
.expect("Failed to create context");
let info = ctx.get_info().await;
log::info!("info: {:#?}", info);
let events = ctx.get_event_emitter();
let events_spawn = tokio::task::spawn(async move {
while let Some(event) = events.recv().await {
cb(event.typ);
}
});
log::info!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 3, "requires email password");
let email = args[1].clone();
let pw = args[2].clone();
ctx.set_config(config::Config::Addr, Some(&email))
.await
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw))
.await
.unwrap();
ctx.configure().await.unwrap();
log::info!("------ RUN ------");
ctx.start_io().await;
log::info!("--- SENDING A MESSAGE ---");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
.await
.unwrap();
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
for i in 0..1 {
log::info!("sending message {}", i);
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
.await
.unwrap();
}
// wait for the message to be sent out
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
log::info!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
for i in 0..chats.len() {
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
.await
.unwrap();
log::info!("[{}] msg: {:?}", i, msg);
}
log::info!("stopping");
ctx.stop_io().await;
log::info!("closing");
drop(ctx);
events_spawn.await.unwrap();
}

View File

@@ -90,7 +90,6 @@ module.exports = {
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,
DC_KEY_GEN_RSA4096: 3,
DC_LP_AUTH_NORMAL: 4,
DC_LP_AUTH_OAUTH2: 2,
DC_MEDIA_QUALITY_BALANCED: 0,
@@ -159,6 +158,8 @@ module.exports = {
DC_STR_BROADCAST_LIST: 115,
DC_STR_CANNOT_LOGIN: 60,
DC_STR_CANTDECRYPT_MSG_BODY: 29,
DC_STR_CHAT_PROTECTION_DISABLED: 171,
DC_STR_CHAT_PROTECTION_ENABLED: 170,
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
@@ -244,12 +245,6 @@ module.exports = {
DC_STR_OUTGOING_MESSAGES: 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_PROTECTION_DISABLED: 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
DC_STR_PROTECTION_ENABLED: 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,

View File

@@ -90,7 +90,6 @@ export enum C {
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_KEY_GEN_RSA4096 = 3,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
@@ -159,6 +158,8 @@ export enum C {
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CHAT_PROTECTION_DISABLED = 171,
DC_STR_CHAT_PROTECTION_ENABLED = 170,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
@@ -244,12 +245,6 @@ export enum C {
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,

View File

@@ -36,7 +36,7 @@ export class Context extends EventEmitter {
}
}
/** Opens a standalone context (without an account manager)
/** Opens a stanalone context (without an account manager)
* automatically starts the event handler */
static open(cwd: string): Context {
const dbFile = join(cwd, 'db.sqlite')
@@ -699,23 +699,6 @@ export class Context extends EventEmitter {
)
}
/**
*
* @param chatId
* @param protect
* @returns success boolean
*/
setChatProtection(chatId: number, protect: boolean) {
debug(`setChatProtection ${chatId} ${protect}`)
return Boolean(
binding.dcn_set_chat_protection(
this.dcn_context,
Number(chatId),
protect ? 1 : 0
)
)
}
getChatEphemeralTimer(chatId: number): number {
debug(`getChatEphemeralTimer ${chatId}`)
return binding.dcn_get_chat_ephemeral_timer(

View File

@@ -21,12 +21,15 @@ export class AccountManager extends EventEmitter {
accountDir: string
jsonRpcStarted = false
constructor(cwd: string, os = 'deltachat-node') {
constructor(cwd: string, writable = true) {
super()
debug('DeltaChat constructor')
this.accountDir = cwd
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
this.dcn_accounts = binding.dcn_accounts_new(
this.accountDir,
writable ? 1 : 0
)
}
getAllAccountIds() {

View File

@@ -1399,18 +1399,6 @@ NAPI_METHOD(dcn_set_chat_name) {
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_set_chat_protection) {
NAPI_ARGV(3);
NAPI_DCN_CONTEXT();
NAPI_ARGV_UINT32(chat_id, 1);
NAPI_ARGV_INT32(protect, 1);
int result = dc_set_chat_protection(dcn_context->dc_context,
chat_id,
protect);
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
NAPI_ARGV(2);
NAPI_DCN_CONTEXT();
@@ -2915,8 +2903,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){
NAPI_METHOD(dcn_accounts_new) {
NAPI_ARGV(2);
NAPI_ARGV_UTF8_MALLOC(os_name, 0);
NAPI_ARGV_UTF8_MALLOC(dir, 1);
NAPI_ARGV_UTF8_MALLOC(dir, 0);
NAPI_ARGV_INT32(writable, 1);
TRACE("calling..");
dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
@@ -2925,7 +2913,7 @@ NAPI_METHOD(dcn_accounts_new) {
}
dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir);
dcn_accounts->dc_accounts = dc_accounts_new(dir, writable);
napi_value result;
NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
@@ -3491,7 +3479,6 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_msg);
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
NAPI_EXPORT_FUNCTION(dcn_set_chat_protection);
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);

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.126.1"
"version": "1.120.0"
}

View File

@@ -24,5 +24,3 @@ ignore_missing_imports = True
[mypy-imap_tools.*]
ignore_missing_imports = True
[mypy-distutils.*]
ignore_missing_imports = True

View File

@@ -34,7 +34,6 @@ dynamic = [
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
"Documentation" = "https://py.delta.chat/"
"Mastodon" = "https://chaos.social/@delta"
[project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin"

View File

@@ -195,7 +195,7 @@ class Account:
assert res != ffi.NULL, f"config value not found for: {name!r}"
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
@@ -203,7 +203,7 @@ class Account:
res = lib.dc_preconfigure_keypair(
self._dc_context,
as_dc_charpointer(addr),
ffi.NULL,
as_dc_charpointer(public),
as_dc_charpointer(secret),
)
if res == 0:
@@ -617,18 +617,18 @@ class Account:
# meta API for start/stop and event based processing
#
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None):
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
from .events import FFIEventLogger
"""get the account running, configure it if necessary. add plugins if provided.
:param addr: the email address of the account
:param password: the password of the account
:param account_plugins: a list of plugins to add
:param show_ffi: show low level ffi events
:param displayname: the display name of the account
"""
from .events import FFIEventLogger
if show_ffi:
self.set_config("displayname", "bot")
log = FFIEventLogger(self)
self.add_account_plugin(log)
@@ -644,8 +644,6 @@ class Account:
configtracker = self.configure()
configtracker.wait_finish()
if displayname:
self.set_config("displayname", displayname)
# start IO threads and configure if necessary
self.start_io()

View File

@@ -75,12 +75,9 @@ class Contact:
"""Return True if the contact is verified."""
return lib.dc_contact_is_verified(self._dc_contact) == 2
def get_verifier(self, contact) -> Optional["Contact"]:
def get_verifier(self, contact):
"""Return the address of the contact that verified the contact."""
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
if verifier_id == 0:
return None
return Contact(self.account, verifier_id)
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
def get_profile_image(self) -> Optional[str]:
"""Get contact profile image.

View File

@@ -478,9 +478,10 @@ class ACFactory:
except IndexError:
pass
else:
fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
if fname_sec:
account._preconfigure_keypair(addr, fname_sec)
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
print(f"WARN: could not use preconfigured keys for {addr!r}")

View File

@@ -1,7 +1,6 @@
import sys
import pytest
import deltachat as dc
class TestGroupStressTests:
@@ -137,6 +136,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: read member added message")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.is_system_message()
assert "added" in msg.text.lower()
lp.sec("ac1: send message")
@@ -150,8 +150,9 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
# If we verified the contact ourselves then verifier addr == contact addr
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
lp.sec("ac2: send message and let ac1 read it")
chat2.send_text("world")
@@ -176,14 +177,15 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
ac2_ac3_contact = ac2.get_contacts()[1]
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
# Skip system message about added member
ac3._evtracker.wait_next_incoming_message()
# System message about the added member.
msg = ac3._evtracker.wait_next_incoming_message()
assert msg.is_system_message()
msg = ac3._evtracker.wait_next_incoming_message()
assert msg.text == "hi"
assert msg.is_encrypted()
@@ -524,7 +526,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
lp.sec("ac2: sending message")
# Message can be sent only after a receipt of "vg-member-added" message. Just wait for
# "Member Me (<addr>) added by <addr>." message.
ac2._evtracker.wait_next_incoming_message()
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.is_system_message()
msg_out = chat2.send_text("hello")
lp.sec("ac1: receiving message")
@@ -539,6 +542,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
assert msg_in.text == msg_out.text
assert msg_in.get_sender_contact().addr == ac2_addr
ac1.set_config("bcc_self", "0")
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
"""Another test for the bug #3836:
@@ -587,67 +592,4 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert msg_in.text == msg_out.text
def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
"""Test for the issue #4346:
- User is added to a verified group.
- First device of the user downloads "member added" from the group.
- First device removes "member added" from the server.
- Some new messages are sent to the group.
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
- First device re-gossips Autocrypt keys to the group.
- Now the seconds device has all members verified.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
ac2.set_config("delete_server_after", "1")
ac2.set_config("gossip_period", "0") # Re-gossip in every message
acfactory.bring_accounts_online()
dir = tmp_path / "exportdir"
dir.mkdir()
ac2.export_self_keys(str(dir))
ac2_offl.import_self_keys(str(dir))
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
# Wait for "Member Me (<addr>) added by <addr>." message.
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.is_system_message()
lp.sec("ac2: waiting for 'member added' to be deleted on the server")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
lp.sec("ac1: sending 'hi' to the group")
ac2.set_config("delete_server_after", "0")
chat1.send_text("hi")
lp.sec("ac2_offl: going online, checking the 'hi' message")
ac2_offl.start_io()
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
assert not msg_in.is_system_message()
assert msg_in.text.startswith("[Sender of this message is not verified:")
ac2_offl_ac1_contact = msg_in.get_sender_contact()
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
assert not ac2_offl_ac1_contact.is_verified()
chat2_offl = msg_in.chat
assert chat2_offl.is_protected()
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
lp.sec("ac2_offl: receiving message")
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert not msg_in.is_system_message()
assert msg_in.text == "hi2"
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert ac2_offl_ac1_contact.is_verified()
ac2.set_config("bcc_self", "0")

View File

@@ -9,7 +9,7 @@ import pytest
from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat import account_hookimpl, Message, Chat
from deltachat.tracker import ImexTracker
@@ -1467,13 +1467,18 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
lp.sec("sending a reaction to the large message from ac1 to ac2")
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
for m in msgs:
ac1._evtracker.wait_msg_delivered(m)
lp.sec("sending a reaction to the large message from ac1 to ac2")
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
# have a later INTERNALDATE.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
ac1._evtracker.wait_msg_delivered(msgs[-1])
ac2.start_io()
lp.sec("wait for ac2 to receive a reaction")
@@ -1505,7 +1510,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
ac1._evtracker.wait_msg_delivered(msg1)
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(2)
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
@@ -1664,8 +1669,12 @@ def test_qr_setup_contact(acfactory, lp):
ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_join_chat(acfactory, lp):
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
@@ -1678,33 +1687,16 @@ def test_qr_join_chat(acfactory, lp):
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac1._evtracker.wait_securejoin_inviter_progress(1000)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
# ac1 reloads the chat.
chat = Chat(chat.account, chat.id)
assert not chat.is_protected()
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
qr = ac1_chat.get_join_qr()
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
ac1_new_chat = ac1.create_group_chat("Another group")
ac1_new_chat.add_contact(ac2)
# Receive "Member added" message.
ac2._evtracker.wait_next_incoming_message()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
# ac2 reloads the chat.
ch = Chat(ch.account, ch.id)
assert not ch.is_protected()
def test_qr_email_capitalization(acfactory, lp):
@@ -1924,15 +1916,13 @@ def test_set_get_group_image(acfactory, data, lp):
lp.sec("ac1: add ac2 to promoted group chat")
chat.add_contact(ac2) # sends one message
lp.sec("ac2: wait for receiving member added message from ac1")
msg1 = ac2._evtracker.wait_next_incoming_message()
assert msg1.is_system_message() # Member added
lp.sec("ac1: send a first message to ac2")
chat.send_text("hi") # sends another message
assert chat.is_promoted()
lp.sec("ac2: wait for receiving message from ac1")
msg1 = ac2._evtracker.wait_next_incoming_message()
assert msg1.is_system_message() # Member added
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "hi"
assert msg1.chat.id == msg2.chat.id

View File

@@ -67,9 +67,10 @@ class TestOfflineAccountBasic:
def test_preconfigure_keypair(self, acfactory, data):
ac = acfactory.get_unconfigured_account()
alice_public = data.read_path("key/alice-public.asc")
alice_secret = data.read_path("key/alice-secret.asc")
assert alice_secret
ac._preconfigure_keypair("alice@example.org", alice_secret)
assert alice_public and alice_secret
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()

View File

@@ -1,4 +1,3 @@
import json
from queue import Queue
import deltachat as dc
@@ -222,31 +221,16 @@ def test_logged_ac_process_ffi_failure(acfactory):
def test_jsonrpc_blocking_call(tmp_path):
accounts_fname = tmp_path / "accounts"
writable = True
accounts = ffi.gc(
lib.dc_accounts_new(ffi.NULL, str(accounts_fname).encode("ascii")),
lib.dc_accounts_new(str(accounts_fname).encode("ascii"), writable),
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
res = json.loads(
from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(
jsonrpc,
json.dumps(
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
).encode("utf-8"),
),
),
res = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
)
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
assert res == "true"
res = json.loads(
from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(
jsonrpc,
json.dumps(
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
).encode("utf-8"),
),
),
)
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"

View File

@@ -25,9 +25,6 @@ deps =
pytest-xdist
pdbpp
requests
# urllib3 2.0 does not work in manylinux2014 containers.
# https://github.com/deltachat/deltachat-core-rust/issues/4788
urllib3<2
[testenv:.pkg]
passenv =
@@ -62,7 +59,8 @@ commands =
[testenv:doc]
changedir=doc
deps =
sphinx
# Pinned due to incompatibility of breathe with sphinx 7.2: <https://github.com/breathe-doc/breathe/issues/943>
sphinx<=7.1.2
breathe
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html

View File

@@ -1 +1 @@
2023-10-24
2023-08-28

View File

@@ -10,8 +10,6 @@ and an own build machine.
- `deny.sh` runs `cargo deny` for all Rust code in the project.
- `codespell.sh` spellchecks the source code using `codespell` tool.
- `../.github/workflows` contains jobs run by GitHub Actions.
- `remote_tests_python.sh` rsyncs to a build machine and runs
@@ -20,7 +18,7 @@ and an own build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
`run-rust-test.sh` remotely on the build machine.
- `make-python-testenv.sh` creates local python test development environment.
- `make-python-testenv.sh` creates or updates local python test development environment.
Reusing the same environment is faster than running `run-python-test.sh` which always
recreates environment from scratch and runs additional lints.

View File

@@ -44,7 +44,7 @@ def file2url(f):
def process_opt(data):
if not "opt" in data:
return "ProviderOptions::new()"
return "Default::default()"
opt = "ProviderOptions {\n"
opt_data = data.get("opt", "")
for key in opt_data:
@@ -54,7 +54,7 @@ def process_opt(data):
if value in {"True", "False"}:
value = value.lower()
opt += " " + key + ": " + value + ",\n"
opt += " ..ProviderOptions::new()\n"
opt += " ..Default::default()\n"
opt += " }"
return opt
@@ -62,7 +62,7 @@ def process_opt(data):
def process_config_defaults(data):
if not "config_defaults" in data:
return "None"
defaults = "Some(&[\n"
defaults = "Some(vec![\n"
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
@@ -96,11 +96,11 @@ def process_data(data, file):
raise TypeError("domain used twice: " + domain)
domains_set.add(domain)
domains += ' ("' + domain + '", &' + file2varname(file) + "),\n"
domains += ' ("' + domain + '", &*' + file2varname(file) + "),\n"
comment += domain + ", "
ids = ""
ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n"
ids += ' ("' + file2id(file) + '", &*' + file2varname(file) + "),\n"
server = ""
has_imap = False
@@ -155,18 +155,18 @@ def process_data(data, file):
provider += (
"static "
+ file2varname(file)
+ ": Provider = Provider {\n"
+ ": Lazy<Provider> = Lazy::new(|| Provider {\n"
)
provider += ' id: "' + file2id(file) + '",\n'
provider += " status: Status::" + status.capitalize() + ",\n"
provider += ' before_login_hint: "' + before_login_hint + '",\n'
provider += ' after_login_hint: "' + after_login_hint + '",\n'
provider += ' overview_page: "' + file2url(file) + '",\n'
provider += " server: &[\n" + server + " ],\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " opt: " + opt + ",\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += "};\n\n"
provider += "});\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")

View File

@@ -4,8 +4,8 @@
# It rebuilds the core and bindings as needed.
#
# After running the script, you can either
# run `pytest` directly with `venv/bin/pytest python/`
# or activate the environment with `. venv/bin/activate`
# run `pytest` directly with `env/bin/pytest python/`
# or activate the environment with `. env/bin/activacte`
# and run `pytest` from there.
set -euo pipefail
@@ -13,5 +13,9 @@ export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
tox -c python -e py --devenv venv
venv/bin/pip install --upgrade pip
if test -d env; then
env/bin/pip install -e python --force-reinstall
else
tox -e py --devenv env
env/bin/pip install --upgrade pip
fi

View File

@@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

View File

@@ -1,197 +0,0 @@
#!/usr/bin/env python3
"""Build Python wheels for deltachat-rpc-server.
Run scripts/zig-rpc-server.sh first."""
from pathlib import Path
from wheel.wheelfile import WheelFile
import tomllib
import tarfile
from io import BytesIO
def metadata_contents(version):
return f"""Metadata-Version: 2.1
Name: deltachat-rpc-server
Version: {version}
Summary: Delta Chat JSON-RPC server
"""
def build_source_package(version):
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
with tarfile.open(filename, "w:gz") as pkg:
def pack(name, contents):
contents = contents.encode()
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))
pack("PKG-INFO", metadata_contents(version))
pack(
"pyproject.toml",
f"""[build-system]
requires = ["setuptools==68.2.2", "pip"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-server"
version = "{version}"
[project.scripts]
deltachat-rpc-server = "deltachat_rpc_server:main"
""",
)
pack(
"setup.py",
f"""
import sys
from setuptools import setup, find_packages
from distutils.cmd import Command
from setuptools.command.install import install
from setuptools.command.build import build
import subprocess
import platform
import tempfile
from zipfile import ZipFile
from pathlib import Path
import shutil
class BuildCommand(build):
def run(self):
tmpdir = tempfile.mkdtemp()
subprocess.run(
[
sys.executable,
"-m",
"pip",
"download",
"--no-input",
"--timeout",
"1000",
"--platform",
"musllinux_1_1_" + platform.machine(),
"--only-binary=:all:",
"deltachat-rpc-server=={version}",
],
cwd=tmpdir,
)
wheel_path = next(Path(tmpdir).glob("*.whl"))
with ZipFile(wheel_path, "r") as wheel:
exe_path = wheel.extract("deltachat_rpc_server/deltachat-rpc-server", "src")
Path(exe_path).chmod(0o700)
wheel.extract("deltachat_rpc_server/__init__.py", "src")
shutil.rmtree(tmpdir)
return super().run()
setup(
cmdclass={{"build": BuildCommand}},
package_data={{"deltachat_rpc_server": ["deltachat-rpc-server"]}},
)
""",
)
pack("src/deltachat_rpc_server/__init__.py", "")
def build_wheel(version, binary, tag, windows=False):
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
with WheelFile(filename, "w") as wheel:
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
wheel.write("deltachat-rpc-server/README.md", "deltachat_rpc_server/README.md")
if windows:
wheel.writestr(
"deltachat_rpc_server/__init__.py",
"""import os, sys, subprocess
def main():
argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server.exe"), *sys.argv[1:]]
sys.exit(subprocess.call(argv))
""",
)
else:
wheel.writestr(
"deltachat_rpc_server/__init__.py",
"""import os, sys
def main():
argv = [os.path.join(os.path.dirname(__file__), "deltachat-rpc-server"), *sys.argv[1:]]
os.execv(argv[0], argv)
""",
)
Path(binary).chmod(0o755)
wheel.write(
binary,
"deltachat_rpc_server/deltachat-rpc-server.exe"
if windows
else "deltachat_rpc_server/deltachat-rpc-server",
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/METADATA",
metadata_contents(version),
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/WHEEL",
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: {tag}",
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/entry_points.txt",
"[console_scripts]\ndeltachat-rpc-server = deltachat_rpc_server:main",
)
def main():
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
cargo_toml = tomllib.load(f)
version = cargo_toml["package"]["version"]
Path("dist").mkdir(exist_ok=True)
build_source_package(version)
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-linux",
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-armv7-linux",
"py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-linux",
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-i686-linux",
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
)
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-macos",
"py3-none-macosx_10_7_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-macos",
"py3-none-macosx_11_0_arm64",
)
build_wheel(
version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
)
build_wheel(
version,
"dist/deltachat-rpc-server-win64.exe",
"py3-none-win_amd64",
windows=True,
)
main()

View File

@@ -1,20 +1,10 @@
#!/usr/bin/env python
# /// pyproject
# [run]
# dependencies = [
# "ziglang==0.11.0"
# ]
# ///
import os
import subprocess
import sys
import os
def flag_filter(flag: str) -> bool:
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
if flag == "-latomic":
return False
if flag == "-lc":
return False
if flag == "-Wl,-melf_i386":
@@ -34,23 +24,8 @@ def main():
else:
zig_cpu_args = []
# Disable atomics and use locks instead in OpenSSL.
# Zig toolchains do not provide atomics.
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
args += ["-DBROKEN_CLANG_ATOMICS"]
subprocess.run(
[
sys.executable,
"-m",
"ziglang",
"cc",
"-target",
zig_target,
*zig_cpu_args,
*args,
],
check=True,
["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True
)

28
scripts/zig-musl-check.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
#
# Run `cargo check` with musl libc.
# This requires `zig` to compile vendored openssl.
set -x
set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
ZIG_VERSION=0.11.0
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
rustup target add x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
cargo check --release --target x86_64-unknown-linux-musl -p deltachat_ffi --features jsonrpc

View File

@@ -10,6 +10,14 @@ unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
ZIG_VERSION=0.11.0
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
rustup target add i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
@@ -42,9 +50,3 @@ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
mkdir -p dist
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux

View File

@@ -43,7 +43,7 @@ the `Subject` header SHOULD be `Message from <sender name>`.
Replies to messages MAY follow the typical `Re:`-format.
The body MAY contain text which MUST have the content type `text/plain`
or `multipart/alternative` containing `text/plain`.
or `mulipart/alternative` containing `text/plain`.
The text MAY be divided into a user-text-part and a footer-part using the
line `-- ` (minus, minus, space, lineend).

View File

@@ -7,6 +7,9 @@ use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tokio::time::{sleep, Duration};
use uuid::Uuid;
use crate::context::Context;
@@ -33,16 +36,16 @@ pub struct Accounts {
impl Accounts {
/// Loads or creates an accounts folder at the given `dir`.
pub async fn new(dir: PathBuf) -> Result<Self> {
if !dir.exists() {
pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
if writable && !dir.exists() {
Accounts::create(&dir).await?;
}
Accounts::open(dir).await
Accounts::open(dir, writable).await
}
/// Creates a new default structure.
pub async fn create(dir: &Path) -> Result<()> {
async fn create(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
.await
.context("failed to create folder")?;
@@ -54,13 +57,13 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
pub async fn open(dir: PathBuf) -> Result<Self> {
async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
ensure!(dir.exists(), "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
let config = Config::from_file(config_file)
let config = Config::from_file(config_file, writable)
.await
.context("failed to load accounts config")?;
let events = Events::new();
@@ -298,14 +301,20 @@ impl Accounts {
/// Configuration file name.
const CONFIG_NAME: &str = "accounts.toml";
/// Lockfile name.
const LOCKFILE_NAME: &str = "accounts.lock";
/// Database file name.
const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug)]
struct Config {
file: PathBuf,
inner: InnerConfig,
// We lock the lockfile in the Config constructors to protect also from having multiple Config
// objects for the same config file.
lock_task: Option<JoinHandle<anyhow::Result<()>>>,
}
/// Account manager configuration file contents.
@@ -319,17 +328,74 @@ struct InnerConfig {
pub accounts: Vec<AccountConfig>,
}
impl Drop for Config {
fn drop(&mut self) {
if let Some(lock_task) = self.lock_task.take() {
lock_task.abort();
}
}
}
impl Config {
/// Creates a new configuration file in the given account manager directory.
pub async fn new(dir: &Path) -> Result<Self> {
/// Creates a new Config for `file`, but doesn't open/sync it.
async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
let dir = file.parent().context("Cannot get config file directory")?;
let inner = InnerConfig {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
};
let file = dir.join(CONFIG_NAME);
let mut cfg = Self { file, inner };
if !lock {
let cfg = Self {
file,
inner,
lock_task: None,
};
return Ok(cfg);
}
let lockfile = dir.join(LOCKFILE_NAME);
let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
let (locked_tx, locked_rx) = oneshot::channel();
let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let mut timeout = Duration::from_millis(100);
let _guard = loop {
match lock.try_write() {
Ok(guard) => break Ok(guard),
Err(err) => {
if timeout.as_millis() > 1600 {
break Err(err);
}
// We need to wait for the previous lock_task to be aborted thus unlocking
// the lockfile. We don't open configs for writing often outside of the
// tests, so this adds delays to the tests, but otherwise ok.
sleep(timeout).await;
if err.kind() == std::io::ErrorKind::WouldBlock {
timeout *= 2;
}
}
}
}?;
locked_tx
.send(())
.ok()
.context("Cannot notify about lockfile locking")?;
let (_tx, rx) = oneshot::channel();
rx.await?;
Ok(())
});
let cfg = Self {
file,
inner,
lock_task: Some(lock_task),
};
locked_rx.await?;
Ok(cfg)
}
/// Creates a new configuration file in the given account manager directory.
pub async fn new(dir: &Path) -> Result<Self> {
let lock = true;
let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
cfg.sync().await?;
Ok(cfg)
@@ -339,6 +405,11 @@ impl Config {
/// Takes a mutable reference because the saved file is a part of the `Config` state. This
/// protects from parallel calls resulting to a wrong file contents.
async fn sync(&mut self) -> Result<()> {
ensure!(!self
.lock_task
.as_ref()
.context("Config is read-only")?
.is_finished());
let tmp_path = self.file.with_extension("toml.tmp");
let mut file = fs::File::create(&tmp_path)
.await
@@ -357,24 +428,28 @@ impl Config {
}
/// Read a configuration from the given file into memory.
pub async fn from_file(file: PathBuf) -> Result<Self> {
let dir = file.parent().context("can't get config file directory")?;
let bytes = fs::read(&file).await.context("failed to read file")?;
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
let dir = file
.parent()
.context("Cannot get config file directory")?
.to_path_buf();
let mut config = Self::new_nosync(file, writable).await?;
let bytes = fs::read(&config.file)
.await
.context("Failed to read file")?;
let s = std::str::from_utf8(&bytes)?;
let mut inner: InnerConfig = toml::from_str(s).context("failed to parse config")?;
config.inner = toml::from_str(s).context("Failed to parse config")?;
// Previous versions of the core stored absolute paths in account config.
// Convert them to relative paths.
let mut modified = false;
for account in &mut inner.accounts {
if let Ok(new_dir) = account.dir.strip_prefix(dir) {
for account in &mut config.inner.accounts {
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
account.dir = new_dir.to_path_buf();
modified = true;
}
}
let mut config = Self { file, inner };
if modified {
if modified && writable {
config.sync().await?;
}
@@ -518,26 +593,44 @@ mod tests {
let p: PathBuf = dir.path().join("accounts1");
{
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
accounts.add_account().await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
{
let accounts = Accounts::open(p).await.unwrap();
for writable in [true, false] {
let accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_new_open_conflict() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
let writable = true;
assert!(Accounts::new(p.clone(), writable).await.is_err());
let writable = false;
let accounts = Accounts::new(p, writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_new_add_remove() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -564,7 +657,8 @@ mod tests {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await?;
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
assert!(accounts.get_selected_account().is_none());
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -585,7 +679,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -622,7 +717,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
for expected_id in 1..10 {
let id = accounts.add_account().await.unwrap();
@@ -642,7 +738,8 @@ mod tests {
let dummy_accounts = 10;
let (id0, id1, id2) = {
let mut accounts = Accounts::new(p.clone()).await?;
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
accounts.add_account().await?;
let ids = accounts.get_all();
assert_eq!(ids.len(), 1);
@@ -677,7 +774,8 @@ mod tests {
assert!(id2 > id1 + dummy_accounts);
let (id0_reopened, id1_reopened, id2_reopened) = {
let accounts = Accounts::new(p.clone()).await?;
let writable = false;
let accounts = Accounts::new(p.clone(), writable).await?;
let ctx = accounts.get_selected_account().unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
@@ -722,7 +820,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let accounts = Accounts::new(p.clone()).await?;
let writable = true;
let accounts = Accounts::new(p.clone(), writable).await?;
// Make sure there are no accounts.
assert_eq!(accounts.accounts.len(), 0);
@@ -748,7 +847,8 @@ mod tests {
let dir = tempfile::tempdir().context("failed to create tempdir")?;
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone())
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable)
.await
.context("failed to create accounts manager")?;
@@ -768,7 +868,8 @@ mod tests {
assert!(passphrase_set_success);
drop(accounts);
let accounts = Accounts::new(p.clone())
let writable = false;
let accounts = Accounts::new(p.clone(), writable)
.await
.context("failed to create second accounts manager")?;
let account = accounts
@@ -792,7 +893,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await?;
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
accounts.add_account().await?;
accounts.add_account().await?;

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
//! # Chat list module.
use anyhow::{ensure, Context as _, Result};
use once_cell::sync::Lazy;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
@@ -11,14 +10,8 @@ use crate::constants::{
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, MsgId};
use crate::param::{Param, Params};
use crate::stock_str;
use crate::summary::Summary;
use crate::tools::IsNoneOrEmpty;
/// Regex to find out if a query should filter by unread messages.
pub static IS_UNREAD_FILTER: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
/// An object representing a single chatlist in memory.
///
@@ -83,8 +76,7 @@ impl Chatlist {
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
/// is added as needed.
/// `query`: An optional query for filtering the list. Only chats matching this query
/// are returned. When `is:unread` is contained in the query, the chatlist is
/// filtered such that only chats with unread messages show up.
/// are returned.
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
/// are returned.
pub async fn try_load(
@@ -178,10 +170,8 @@ impl Chatlist {
)
.await?
} else if let Some(query) = query {
let mut query = query.trim().to_string();
ensure!(!query.is_empty(), "query mustn't be empty");
let only_unread = IS_UNREAD_FILTER.find(&query).is_some();
query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string();
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
// allow searching over special names that may change at any time
// when the ui calls set_stock_translation()
@@ -206,93 +196,42 @@ impl Chatlist {
WHERE c.id>9 AND c.id!=?2
AND c.blocked!=1
AND c.name LIKE ?3
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
(MessageState::OutDraft, skip_id, str_like_cmd),
process_row,
process_rows,
)
.await?
} else {
let mut ids = if flag_for_forwarding {
let sort_id_up = ChatId::lookup_by_contact(context, ContactId::SELF)
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
.unwrap_or_default();
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let typ: Chattype = row.get(1)?;
let param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
let msg_id: Option<MsgId> = row.get(3)?;
Ok((chat_id, typ, param, msg_id))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
Ok((chat_id, typ, param, msg_id)) => {
if typ == Chattype::Mailinglist
&& param.get(Param::ListPost).is_none_or_empty()
{
None
} else {
Some(Ok((chat_id, msg_id)))
}
}
Err(e) => Some(Err(e)),
})
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
// time. It may be confusing if a chat that is normally in the list disappears
// suddenly. The UI need to deal with that case anyway.
context.sql.query_map(
"SELECT c.id, c.type, c.param, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?
AND c.blocked=0
AND NOT c.archived=?
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?))
GROUP BY c.id
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
Chattype::Group, ContactId::SELF,
sort_id_up, ChatVisibility::Pinned,
),
process_row,
process_rows,
).await?
.unwrap_or_default()
} else {
// show normal chatlist
context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?
AND (c.blocked=0 OR c.blocked=2)
AND NOT c.archived=?
GROUP BY c.id
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
process_row,
process_rows,
).await?
ChatId::new(0)
};
let mut ids = context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND (c.blocked=0 OR (c.blocked=2 AND NOT ?3))
AND NOT c.archived=?4
GROUP BY c.id
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned),
process_row,
process_rows,
).await?;
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
@@ -305,27 +244,6 @@ impl Chatlist {
Ok(Chatlist { ids })
}
/// Converts list of chat IDs to a chatlist.
pub(crate) async fn from_chat_ids(context: &Context, chat_ids: &[ChatId]) -> Result<Self> {
let mut ids = Vec::new();
for &chat_id in chat_ids {
let msg_id: Option<MsgId> = context
.sql
.query_get_value(
"SELECT id
FROM msgs
WHERE chat_id=?1
AND (hidden=0 OR state=?2)
ORDER BY timestamp DESC, id DESC LIMIT 1",
(chat_id, MessageState::OutDraft),
)
.await
.with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
ids.push((chat_id, msg_id));
}
Ok(Chatlist { ids })
}
/// Find out the number of chats.
pub fn len(&self) -> usize {
self.ids.len()
@@ -406,7 +324,7 @@ impl Chatlist {
.context("loading contact failed")?;
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
Chattype::Single => (Some(lastmsg), None),
}
}
} else {
@@ -470,10 +388,7 @@ pub async fn get_last_message_for_chat(
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
};
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
@@ -481,7 +396,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() {
let t = TestContext::new_bob().await;
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
@@ -520,31 +435,6 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
assert_eq!(chats.len(), 1);
// receive a message from alice
let alice = TestContext::new_alice().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
.await
.unwrap();
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "bob", "bob@example.net")
.await
.unwrap(),
)
.await
.unwrap();
send_text_msg(&alice, alice_chat_id, "hi".into())
.await
.unwrap();
let sent_msg = alice.pop_sent_msg().await;
t.recv_msg(&sent_msg).await;
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
.await
.unwrap();
assert!(chats.len() == 1);
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
@@ -583,14 +473,6 @@ mod tests {
.await
.unwrap()
.is_self_talk());
remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF)
.await
.unwrap();
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
.await
.unwrap();
assert!(chats.len() == 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -1,7 +1,6 @@
//! # Key-value configuration management.
use std::env;
use std::path::Path;
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result};
@@ -286,12 +285,6 @@ pub enum Config {
#[strum(props(default = "60"))]
ScanAllFoldersDebounceSecs,
/// Whether to avoid using IMAP IDLE even if the server supports it.
///
/// This is a developer option for testing "fake idle".
#[strum(props(default = "0"))]
DisableIdle,
/// Defines the max. size (in bytes) of messages downloaded automatically.
/// 0 = no limit.
#[strum(props(default = "0"))]
@@ -319,12 +312,15 @@ pub enum Config {
/// Last message processed by the bot.
LastMsgId,
/// 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.
#[strum(props(default = "172800"))]
GossipPeriod,
/// Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
/// and when the key changes, an info message is posted into the chat.
/// 0=Nothing else happens when the key changes.
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
/// until `chat_id.accept()` is called.
#[strum(props(default = "0"))]
VerifiedOneOnOneChats,
}
impl Context {
@@ -343,11 +339,7 @@ impl Context {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
rel_path.map(|p| {
get_abs_path(self, Path::new(&p))
.to_string_lossy()
.into_owned()
})
rel_path.map(|p| get_abs_path(self, p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
@@ -478,28 +470,6 @@ impl Context {
.set_raw_config(key.as_ref(), value.as_deref())
.await?;
}
Config::Socks5Enabled
| Config::BccSelf
| Config::E2eeEnabled
| Config::MdnsEnabled
| Config::SentboxWatch
| Config::MvboxMove
| Config::OnlyFetchMvbox
| Config::FetchExistingMsgs
| Config::DeleteToTrash
| Config::SaveMimeHeaders
| Config::Configured
| Config::Bot
| Config::NotifyAboutWrongPw
| Config::SendSyncMsgs
| Config::SignUnencrypted
| Config::DisableIdle => {
ensure!(
matches!(value, None | Some("0") | Some("1")),
"Boolean value must be either 0 or 1"
);
self.sql.set_raw_config(key.as_ref(), value).await?;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -649,18 +619,6 @@ mod tests {
);
}
/// Tests that "bot" config can only be set to "0" or "1".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bot() {
let t = TestContext::new().await;
assert!(t.set_config(Config::Bot, None).await.is_ok());
assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;

View File

@@ -26,6 +26,7 @@ use crate::config::Config;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::imap::Imap;
use crate::job;
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
@@ -129,7 +130,7 @@ async fn on_configure_completed(
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = param.provider {
if let Some(config_defaults) = provider.config_defaults {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults {
if !context.config_exists(def.key).await? {
info!(context, "apply config_defaults {}={}", def.key, def.value);
@@ -465,7 +466,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
if configured_addr != param.addr {
// Switched account, all server UIDs we know are invalid
info!(ctx, "Scheduling resync because the address has changed.");
ctx.schedule_resync().await?;
job::schedule_resync(ctx).await?;
}
}

View File

@@ -62,15 +62,8 @@ pub enum MediaQuality {
pub enum KeyGenType {
#[default]
Default = 0,
/// 2048-bit RSA.
Rsa2048 = 1,
/// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
Ed25519 = 2,
/// 4096-bit RSA.
Rsa4096 = 3,
}
/// Video chat URL type.
@@ -125,7 +118,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
/// Chat type.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -141,10 +133,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
)]
#[repr(u32)]
pub enum Chattype {
/// Undefined chat type.
#[default]
Undefined = 0,
/// 1:1 chat.
Single = 100,
@@ -223,8 +211,6 @@ mod tests {
#[test]
fn test_chattype_values() {
// values may be written to disk and must not change
assert_eq!(Chattype::Undefined, Chattype::default());
assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap());
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
@@ -238,7 +224,6 @@ mod tests {
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
}
#[test]

View File

@@ -5,7 +5,7 @@ use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, ensure, Context as _, Result};
@@ -25,7 +25,7 @@ use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{DcKey, SignedPublicKey};
use crate::key::{load_self_public_key, DcKey};
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
@@ -109,7 +109,7 @@ impl ContactId {
/// ID of the contact for device messages.
pub const DEVICE: ContactId = ContactId::new(5);
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
const LAST_SPECIAL: ContactId = ContactId::new(9);
/// Address to go with [`ContactId::DEVICE`].
///
@@ -812,11 +812,7 @@ impl Contact {
let mut ret = Vec::new();
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
Origin::Unknown
} else {
Origin::IncomingReplyTo
};
if flag_verified_only || query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
context
@@ -836,7 +832,7 @@ impl Contact {
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
minimal_origin,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 }
@@ -886,10 +882,10 @@ impl Contact {
ORDER BY last_seen DESC, id DESC;",
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(
params_iter(&self_addrs)
.chain(params_slice![ContactId::LAST_SPECIAL, minimal_origin]),
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
Origin::IncomingReplyTo
])),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
@@ -1013,7 +1009,7 @@ impl Contact {
let finger_prints = stock_str::finger_prints(context).await;
ret += &format!("{stock_message}.\n{finger_prints}:");
let fingerprint_self = SignedPublicKey::load_self(context)
let fingerprint_self = load_self_public_key(context)
.await?
.fingerprint()
.to_string();
@@ -1190,7 +1186,7 @@ impl Contact {
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
return Ok(Some(get_abs_path(context, image_rel)));
}
}
Ok(None)
@@ -1215,31 +1211,15 @@ impl Contact {
/// and if the key has not changed since this verification.
///
/// The UI may draw a checkbox or something like that beside verified contacts.
///
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
self.is_verified_ex(context, None).await
}
/// Same as `Contact::is_verified` but allows speeding up things
/// by adding the peerstate belonging to the contact.
/// If you do not have the peerstate available, it is loaded automatically.
pub async fn is_verified_ex(
&self,
context: &Context,
peerstate: Option<&Peerstate>,
) -> Result<VerifiedStatus> {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
if self.id == ContactId::SELF {
return Ok(VerifiedStatus::BidirectVerified);
}
if let Some(peerstate) = peerstate {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
} else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.verified_key.is_some() {
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.is_using_verified_key() {
return Ok(VerifiedStatus::BidirectVerified);
}
}
@@ -1256,22 +1236,11 @@ impl Contact {
/// Returns the ContactId that verified the contact.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
return Ok(None);
};
if verifier_addr == self.addr {
// Contact is directly verified via QR code.
return Ok(Some(ContactId::SELF));
}
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? {
Some(contact_id) => Ok(Some(contact_id)),
None => {
let addr = &self.addr;
warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
Ok(None)
}
let verifier_addr = self.get_verifier_addr(context).await?;
if let Some(addr) = verifier_addr {
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
} else {
Ok(None)
}
}
@@ -1747,7 +1716,7 @@ mod tests {
assert_eq!(may_be_valid_addr("dd.tt"), false);
assert_eq!(may_be_valid_addr("tt.dd@uu"), true);
assert_eq!(may_be_valid_addr("u@d"), true);
assert_eq!(may_be_valid_addr("u@d."), false);
assert_eq!(may_be_valid_addr("u@d."), true);
assert_eq!(may_be_valid_addr("u@d.t"), true);
assert_eq!(may_be_valid_addr("u@d.tt"), true);
assert_eq!(may_be_valid_addr("u@.tt"), true);
@@ -1756,7 +1725,6 @@ mod tests {
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
assert_eq!(may_be_valid_addr("as@sd.de>"), false);
assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false);
assert_eq!(may_be_valid_addr("user@domain.tld."), false);
}
#[test]

View File

@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
@@ -19,11 +19,11 @@ use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::key::{load_self_public_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::{InterruptInfo, SchedulerState};
use crate::scheduler::SchedulerState;
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -38,7 +38,7 @@ use crate::tools::{duration_to_str, time};
///
/// # Examples
///
/// Creating a new unencrypted database:
/// Creating a new unecrypted database:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
@@ -211,6 +211,9 @@ pub struct InnerContext {
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
/// Set to true if quota update is requested.
pub(crate) quota_update_request: AtomicBool,
/// IMAP UID resync request.
pub(crate) resync_request: AtomicBool,
@@ -329,12 +332,6 @@ impl Context {
}
}
/// Changes encrypted database passphrase.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
self.sql.change_passphrase(passphrase).await?;
Ok(())
}
/// Returns true if database is open.
pub async fn is_open(&self) -> bool {
self.sql.is_open().await
@@ -379,8 +376,9 @@ impl Context {
translated_stockstrings: stockstrings,
events,
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
new_msgs_notify,
server_id: RwLock::new(None),
@@ -422,16 +420,6 @@ impl Context {
self.scheduler.maybe_network().await;
}
pub(crate) async fn schedule_resync(&self) -> Result<()> {
self.resync_request.store(true, Ordering::Relaxed);
self.scheduler
.interrupt_inbox(InterruptInfo {
probe_network: false,
})
.await;
Ok(())
}
/// Returns a reference to the underlying SQL instance.
///
/// Warning: this is only here for testing, not part of the public API.
@@ -585,7 +573,6 @@ impl Context {
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
@@ -593,7 +580,7 @@ impl Context {
.sql
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
let fingerprint_str = match SignedPublicKey::load_self(self).await {
let fingerprint_str = match load_self_public_key(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {err}>"),
};
@@ -698,7 +685,6 @@ impl Context {
);
res.insert("bcc_self", bcc_self.to_string());
res.insert("send_sync_msgs", send_sync_msgs.to_string());
res.insert("disable_idle", disable_idle.to_string());
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
res.insert("fingerprint", fingerprint_str);
@@ -760,6 +746,7 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
@@ -769,8 +756,10 @@ impl Context {
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
res.insert(
"gossip_period",
self.get_config_int(Config::GossipPeriod).await?.to_string(),
"verified_one_on_one_chats",
self.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
@@ -825,22 +814,7 @@ impl Context {
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
Some(s) => MsgId::new(s.parse()?),
None => {
// If `last_msg_id` is not set yet,
// subtract 1 from the last id,
// so a single message is returned and can
// be marked as seen.
self.sql
.query_row(
"SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
(),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
)
.await?
}
None => MsgId::new_unset(),
};
let list = self
@@ -1485,35 +1459,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_context_change_passphrase() -> Result<()> {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
context
.set_config(Config::Addr, Some("alice@example.org"))
.await?;
context
.change_passphrase("bar".to_string())
.await
.context("Failed to change passphrase")?;
assert_eq!(
context.get_config(Config::Addr).await?.unwrap(),
"alice@example.org"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ongoing() -> Result<()> {
let context = TestContext::new().await;

View File

@@ -1,14 +1,12 @@
//! Forward log messages to logging webxdc
use crate::{
chat::ChatId,
config::Config,
context::Context,
message::{Message, MsgId, Viewtype},
param::Param,
tools::time,
webxdc::StatusUpdateItem,
EventType,
};
use crate::chat::ChatId;
use crate::config::Config;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{Message, MsgId, Viewtype};
use crate::param::Param;
use crate::tools::time;
use crate::webxdc::StatusUpdateItem;
use async_channel::{self as channel, Receiver, Sender};
use serde_json::json;
use std::path::PathBuf;

View File

@@ -13,7 +13,6 @@ use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::peerstate::Peerstate;
use crate::pgp;
@@ -26,33 +25,17 @@ use crate::pgp;
pub fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
private_keyring: &Keyring<SignedSecretKey>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let encrypted_data_part = match {
let mime = get_autocrypt_mime(mail);
if mime.is_some() {
info!(context, "Detected Autocrypt-mime message.");
}
mime
}
.or_else(|| {
let mime = get_mixed_up_mime(mail);
if mime.is_some() {
info!(context, "Detected mixed-up mime message.");
}
mime
})
.or_else(|| {
let mime = get_attachment_mime(mail);
if mime.is_some() {
info!(context, "Detected attached Autocrypt-mime message.");
}
mime
}) {
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => return Ok(None),
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
decrypt_part(
encrypted_data_part,
@@ -227,8 +210,8 @@ 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: &Keyring<SignedSecretKey>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let data = mail.get_body_raw()?;
@@ -263,7 +246,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts.
pub(crate) fn validate_detached_signature<'a, 'b>(
mail: &'a ParsedMail<'b>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
public_keyring_for_validate: &[SignedPublicKey],
) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> {
if mail.ctype.mimetype != "multipart/signed" {
return None;
@@ -283,13 +266,13 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
}
}
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring<SignedPublicKey> {
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<SignedPublicKey> {
let mut public_keyring_for_validate = Vec::new();
if let Some(peerstate) = peerstate {
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
public_keyring_for_validate.push(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
public_keyring_for_validate.push(key.clone());
}
}
public_keyring_for_validate
@@ -419,18 +402,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mixed_up_mime_long() -> Result<()> {
// Long "mixed-up" mail as received when sending an encrypted message using Delta Chat
// Desktop via MS Exchange (actually made with TB though).
let mixed_up_mime = include_bytes!("../test-data/message/mixed-up-long.eml");
let bob = TestContext::new_bob().await;
receive_imf(&bob, mixed_up_mime, false).await?;
let msg = bob.get_last_msg().await;
assert!(!msg.get_text().is_empty());
assert!(msg.has_html());
assert!(msg.id.get_html(&bob).await?.unwrap().len() > 40000);
Ok(())
}
}

View File

@@ -10,11 +10,11 @@ use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context;
use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::scheduler::InterruptInfo;
use crate::tools::time;
use crate::{stock_str, EventType};
use crate::{job_try, stock_str, EventType};
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
@@ -23,7 +23,7 @@ use crate::{stock_str, EventType};
/// eg. to assign them to the correct chat.
/// As these messages are typically small,
/// they're caught by `MIN_DOWNLOAD_LIMIT`.
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
const MIN_DOWNLOAD_LIMIT: u32 = 32768;
/// If a message is downloaded only partially
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
@@ -59,9 +59,6 @@ pub enum DownloadState {
/// Failed to fully download the message.
Failure = 20,
/// Undecipherable message.
Undecipherable = 30,
/// Full download of the message is in progress.
InProgress = 1000,
}
@@ -83,21 +80,12 @@ impl MsgId {
pub async fn download_full(self, context: &Context) -> Result<()> {
let msg = Message::load_from_db(context, self).await?;
match msg.download_state() {
DownloadState::Done | DownloadState::Undecipherable => {
return Err(anyhow!("Nothing to download."))
}
DownloadState::Done => return Err(anyhow!("Nothing to download.")),
DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
DownloadState::Available | DownloadState::Failure => {
self.update_download_state(context, DownloadState::InProgress)
.await?;
context
.sql
.execute("INSERT INTO download (msg_id) VALUES (?)", (self,))
.await?;
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
job::add(context, Job::new(Action::DownloadMsg, self.to_u32())).await?;
}
}
Ok(())
@@ -131,49 +119,59 @@ impl Message {
}
}
/// Actually download a message partially downloaded before.
///
/// Most messages are downloaded automatically on fetch instead.
pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Imap) -> Result<()> {
imap.prepare(context).await?;
impl Job {
/// Actually download a message.
/// Called in response to `Action::DownloadMsg`.
pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "download: could not connect: {:#}", err);
return Status::RetryNow;
}
let msg = Message::load_from_db(context, msg_id).await?;
let row = context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
(&msg.rfc724_mid,),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
Ok((server_uid, server_folder))
},
)
.await?;
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let row = job_try!(
context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder",
(&msg.rfc724_mid,),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
Ok((server_uid, server_folder))
}
)
.await
);
if let Some((server_uid, server_folder)) = row {
match imap
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
if let Some((server_uid, server_folder)) = row {
match imap
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
job_try!(
msg.id
.update_download_state(context, DownloadState::Failure)
.await
);
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
}
ImapActionResult::Success => {
// update_download_state() not needed as receive_imf() already
// set the state and emitted the event.
Status::Finished(Ok(()))
}
}
} else {
// No IMAP record found, we don't know the UID and folder.
job_try!(
msg.id
.update_download_state(context, DownloadState::Failure)
.await?;
Err(anyhow!("Call download_full() again to try over."))
}
ImapActionResult::Success => {
// update_download_state() not needed as receive_imf() already
// set the state and emitted the event.
Ok(())
}
.await
);
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
}
} else {
// No IMAP record found, we don't know the UID and folder.
msg.id
.update_download_state(context, DownloadState::Failure)
.await?;
Err(anyhow!("Call download_full() again to try over."))
}
}

View File

@@ -6,8 +6,7 @@ use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::context::Context;
use crate::key::{DcKey, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::pgp;
@@ -24,7 +23,7 @@ impl EncryptHelper {
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let addr = context.get_primary_self_addr().await?;
let public_key = SignedPublicKey::load_self(context).await?;
let public_key = load_self_public_key(context).await?;
Ok(EncryptHelper {
prefer_encrypt,
@@ -104,7 +103,7 @@ impl EncryptHelper {
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, &str)>,
) -> Result<String> {
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
let mut keyring: Vec<SignedPublicKey> = Vec::new();
for (peerstate, addr) in peerstates
.into_iter()
@@ -113,10 +112,10 @@ impl EncryptHelper {
let key = peerstate
.take_key(min_verified)
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
keyring.add(key);
keyring.push(key);
}
keyring.add(self.public_key.clone());
let sign_key = SignedSecretKey::load_self(context).await?;
keyring.push(self.public_key.clone());
let sign_key = load_self_secret_key(context).await?;
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
@@ -132,7 +131,7 @@ impl EncryptHelper {
context: &Context,
mail: lettre_email::PartBuilder,
) -> Result<(lettre_email::MimeMessage, String)> {
let sign_key = SignedSecretKey::load_self(context).await?;
let sign_key = load_self_secret_key(context).await?;
let mime_message = mail.build();
let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?;
Ok((mime_message, signature))
@@ -145,20 +144,17 @@ impl EncryptHelper {
/// sent but in a few locations there are no such guarantees,
/// e.g. when exporting keys, and calling this function ensures a
/// private key will be present.
///
/// If this succeeds you are also guaranteed that the
/// [Config::ConfiguredAddr] is configured, this address is returned.
// TODO, remove this once deltachat::key::Key no longer exists.
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context.get_primary_self_addr().await?;
SignedPublicKey::load_self(context).await?;
Ok(self_addr)
pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
load_self_public_key(context).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::key::DcKey;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::test_utils::{bob_keypair, TestContext};
@@ -169,10 +165,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prexisting() {
let t = TestContext::new_alice().await;
assert_eq!(
ensure_secret_key_exists(&t).await.unwrap(),
"alice@example.org"
);
assert!(ensure_secret_key_exists(&t).await.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -545,7 +545,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
ephemeral_timestamp
.into_iter()
.chain(delete_device_after_timestamp.into_iter())
.chain(delete_device_after_timestamp)
.min()
}
@@ -986,7 +986,7 @@ mod tests {
t.send_text(self_chat.id, "Saved message, which we delete manually")
.await;
let msg = t.get_last_msg_in(self_chat.id).await;
msg.id.delete_from_db(&t).await?;
msg.id.trash(&t).await?;
check_msg_is_deleted(&t, &self_chat, msg.id).await;
self_chat
@@ -1003,7 +1003,7 @@ mod tests {
.await
.unwrap();
// Set DeleteDeviceAfter to 1800s. Thend send a saved message which will
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
.await?;
@@ -1260,8 +1260,8 @@ mod tests {
);
let msg = alice.get_last_msg().await;
// Message is deleted from the database when its timer expires.
msg.id.delete_from_db(&alice).await?;
// Message is deleted when its timer expires.
msg.id.trash(&alice).await?;
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
// <second@example.com>, is received. The message <second@example.come> is not in the

View File

@@ -17,12 +17,12 @@ use lettre_email::mime::{self, Mime};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
use crate::message::{self, Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
impl Message {
/// Check if the message can be retrieved as HTML.

View File

@@ -26,6 +26,7 @@ use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifie
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
@@ -81,6 +82,7 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
)])";
const JUST_UID: &str = "(UID)";
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
@@ -389,7 +391,6 @@ impl Imap {
"IMAP-LOGIN as {}",
self.config.lp.user
)));
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
Ok(())
}
@@ -613,7 +614,7 @@ impl Imap {
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
);
set_uid_next(context, folder, uid_next).await?;
context.schedule_resync().await?;
job::schedule_resync(context).await?;
}
uid_next != old_uid_next // If uid_next changed, there are new emails
} else {
@@ -625,6 +626,18 @@ impl Imap {
// UIDVALIDITY is modified, reset highest seen MODSEQ.
set_modseq(context, folder, 0).await?;
if mailbox.exists == 0 {
info!(context, "Folder {folder:?} is empty.");
// set uid_next=1 for empty folders.
// If we do not do this here, we'll miss the first message
// as we will get in here again and fetch from uid_next then.
// Also, the "fall back to fetching" below would need a non-zero mailbox.exists to work.
set_uid_next(context, folder, 1).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
return Ok(false);
}
// ============== uid_validity has changed or is being set the first time. ==============
let new_uid_next = match mailbox.uid_next {
@@ -632,35 +645,25 @@ impl Imap {
None => {
warn!(
context,
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
"IMAP folder {folder:?} has no uid_next, fall back to fetching."
);
// RFC 3501 says STATUS command SHOULD NOT be used
// on the currently selected mailbox because the same
// information can be obtained by other means,
// such as reading SELECT response.
//
// However, it also says that UIDNEXT is REQUIRED
// in the SELECT response and if we are here,
// it is actually not returned.
//
// In particular, Winmail Pro Mail Server 5.1.0616
// never returns UIDNEXT in SELECT response,
// but responds to "SELECT INBOX (UIDNEXT)" command.
let status = session
// note that we use fetch by sequence number
// and thus we only need to get exactly the
// last-index message.
let set = format!("{}", mailbox.exists);
let mut list = session
.inner
.status(folder, "(UIDNEXT)")
.fetch(set, JUST_UID)
.await
.context("STATUS (UIDNEXT) error for {folder:?}")?;
.context("Error fetching UID")?;
if let Some(uid_next) = status.uid_next {
uid_next
} else {
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
// Set UIDNEXT to 1 as a last resort fallback.
1
let mut new_last_seen_uid = None;
while let Some(fetch) = list.try_next().await? {
if fetch.message == mailbox.exists && fetch.uid.is_some() {
new_last_seen_uid = fetch.uid;
}
}
new_last_seen_uid.context("select: failed to fetch")? + 1
}
};
@@ -677,7 +680,7 @@ impl Imap {
.await?;
if old_uid_validity != 0 || old_uid_next != 0 {
context.schedule_resync().await?;
job::schedule_resync(context).await?;
}
info!(
context,

View File

@@ -15,7 +15,6 @@ use crate::net::connect_tcp;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::socks::Socks5Config;
use fast_socks5::client::Socks5Stream;
/// IMAP write and read timeout.
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
@@ -65,12 +64,6 @@ async fn determine_capabilities(
}
impl Client {
fn new(stream: Box<dyn SessionStream>) -> Self {
Self {
inner: ImapClient::new(stream),
}
}
pub(crate) async fn login(self, username: &str, password: &str) -> Result<Session> {
let Client { inner, .. } = self;
let mut session = inner
@@ -105,24 +98,27 @@ impl Client {
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let mut client = ImapClient::new(session_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
Ok(client)
Ok(Client { inner: client })
}
pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?;
let buffered_stream = BufWriter::new(tcp_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let mut client = ImapClient::new(session_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
Ok(client)
Ok(Client { inner: client })
}
pub async fn connect_starttls(
@@ -134,8 +130,7 @@ impl Client {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
// Run STARTTLS command and convert the client back into a stream.
let buffered_tcp_stream = BufWriter::new(tcp_stream);
let mut client = ImapClient::new(buffered_tcp_stream);
let mut client = ImapClient::new(tcp_stream);
let _greeting = client
.read_response()
.await
@@ -144,8 +139,7 @@ impl Client {
.run_command_and_check_ok("STARTTLS", None)
.await
.context("STARTTLS command failed")?;
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tcp_stream = client.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
.await
@@ -153,8 +147,9 @@ impl Client {
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);
Ok(client)
let client = ImapClient::new(session_stream);
Ok(Client { inner: client })
}
pub async fn connect_secure_socks5(
@@ -170,12 +165,13 @@ impl Client {
let tls_stream = wrap_tls(strict_tls, domain, socks5_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let mut client = ImapClient::new(session_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
Ok(client)
Ok(Client { inner: client })
}
pub async fn connect_insecure_socks5(
@@ -189,12 +185,13 @@ impl Client {
.await?;
let buffered_stream = BufWriter::new(socks5_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let mut client = ImapClient::new(session_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
Ok(client)
Ok(Client { inner: client })
}
pub async fn connect_starttls_socks5(
@@ -209,8 +206,7 @@ impl Client {
.await?;
// Run STARTTLS command and convert the client back into a stream.
let buffered_socks5_stream = BufWriter::new(socks5_stream);
let mut client = ImapClient::new(buffered_socks5_stream);
let mut client = ImapClient::new(socks5_stream);
let _greeting = client
.read_response()
.await
@@ -219,15 +215,15 @@ impl Client {
.run_command_and_check_ok("STARTTLS", None)
.await
.context("STARTTLS command failed")?;
let buffered_socks5_stream = client.into_inner();
let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner();
let socks5_stream = client.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, socks5_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);
Ok(client)
let client = ImapClient::new(session_stream);
Ok(Client { inner: client })
}
}

View File

@@ -7,9 +7,7 @@ use futures_lite::FutureExt;
use super::session::Session;
use super::Imap;
use crate::config::Config;
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::log::LogExt;
use crate::{context::Context, scheduler::InterruptInfo};
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
@@ -23,10 +21,6 @@ impl Session {
) -> Result<(Self, InterruptInfo)> {
use futures::future::FutureExt;
if context.get_config_bool(Config::DisableIdle).await? {
bail!("IMAP IDLE is disabled");
}
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
@@ -169,14 +163,7 @@ impl Imap {
continue;
}
if let Some(session) = &self.session {
if session.can_idle()
&& !context
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(context)
.unwrap_or_default()
{
if session.can_idle() {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false);
}

View File

@@ -19,7 +19,9 @@ use crate::contact::ContactId;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::key::{
self, load_self_secret_key, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey,
};
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
@@ -58,7 +60,8 @@ pub enum ImexMode {
/// Export a backup to the directory given as `path` with the given `passphrase`.
/// The backup contains all contacts, chats, images and other data and device independent settings.
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
/// The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
/// The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
/// the format is `delta-chat-<day>-<number>.tar`
ExportBackup = 11,
/// `path` is the file (not: directory) to import. The file is normally
@@ -127,7 +130,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
// We just use string comparison to determine which backup is newer.
// This works fine because the filenames have the form `delta-chat-backup-2023-10-18-00-foo@example.com.tar`
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar
newest_backup_path = Some(path);
newest_backup_name = name;
}
@@ -185,7 +188,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
} else {
bail!("Passphrase must be at least 2 chars long.");
};
let private_key = SignedSecretKey::load_self(context).await?;
let private_key = load_self_secret_key(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
@@ -483,11 +486,7 @@ async fn import_backup(
/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete.
fn get_next_backup_path(
folder: &Path,
addr: &str,
backup_time: i64,
) -> Result<(PathBuf, PathBuf, PathBuf)> {
fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, PathBuf, PathBuf)> {
let folder = PathBuf::from(folder);
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
.context("can't get next backup path")?
@@ -498,13 +497,13 @@ fn get_next_backup_path(
// 64 backup files per day should be enough for everyone
for i in 0..64 {
let mut tempdbfile = folder.clone();
tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
tempdbfile.push(format!("{stem}-{i:02}.db"));
let mut tempfile = folder.clone();
tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
tempfile.push(format!("{stem}-{i:02}.tar.part"));
let mut destfile = folder.clone();
destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
destfile.push(format!("{stem}-{i:02}.tar"));
if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
return Ok((tempdbfile, tempfile, destfile));
@@ -519,8 +518,7 @@ fn get_next_backup_path(
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time();
let self_addr = context.get_primary_self_addr().await?;
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now)?;
let _d1 = DeleteOnDrop(temp_db_path.clone());
let _d2 = DeleteOnDrop(temp_path.clone());
@@ -590,74 +588,63 @@ async fn export_backup_inner(
Ok(())
}
/// Imports secret key from a file.
async fn import_secret_key(context: &Context, path: &Path, set_default: bool) -> Result<()> {
let buf = read_file(context, &path).await?;
let armored = std::string::String::from_utf8_lossy(&buf);
set_self_key(context, &armored, set_default, false).await?;
Ok(())
}
/// Imports secret keys from the provided file or directory.
///
/// If provided path is a file, ASCII-armored secret key is read from the file
/// and set as the default key.
///
/// If provided path is a directory, all files with .asc extension
/// containing secret keys are imported and the last successfully
/// imported which does not contain "legacy" in its filename
/// is set as the default.
async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
let attr = tokio::fs::metadata(path).await?;
if attr.is_file() {
info!(
context,
"Importing secret key from {} as the default key.",
path.display()
);
let set_default = true;
import_secret_key(context, path, set_default).await?;
return Ok(());
}
/*******************************************************************************
* Classic key import
******************************************************************************/
async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
plain ASC keys, at least keys without a password, if we do not want to implement a password entry function.
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
Maybe we should make the "default" key handlong also a little bit smarter
(currently, the last imported key is the standard key unless it contains the string "legacy" in its name) */
let mut set_default: bool;
let mut imported_cnt = 0;
let mut dir_handle = tokio::fs::read_dir(&path).await?;
let dir_name = dir.to_string_lossy();
let mut dir_handle = tokio::fs::read_dir(&dir).await?;
while let Ok(Some(entry)) = dir_handle.next_entry().await {
let entry_fn = entry.file_name();
let name_f = entry_fn.to_string_lossy();
let path_plus_name = path.join(&entry_fn);
if let Some(suffix) = get_filesuffix_lc(&name_f) {
if suffix != "asc" {
let path_plus_name = dir.join(&entry_fn);
match get_filesuffix_lc(&name_f) {
Some(suffix) => {
if suffix != "asc" {
continue;
}
set_default = if name_f.contains("legacy") {
info!(context, "found legacy key '{}'", path_plus_name.display());
false
} else {
true
}
}
None => {
continue;
}
} else {
continue;
};
let set_default = !name_f.contains("legacy");
}
info!(
context,
"Considering key file: {}.",
"considering key file: {}",
path_plus_name.display()
);
if let Err(err) = import_secret_key(context, &path_plus_name, set_default).await {
warn!(
context,
"Failed to import secret key from {}: {:#}.",
path_plus_name.display(),
err
);
continue;
match read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
info!(context, "set_self_key: {}", err);
continue;
}
}
Err(_) => continue,
}
imported_cnt += 1;
}
ensure!(
imported_cnt > 0,
"No private keys found in {}.",
path.display()
"No private keys found in \"{}\".",
dir_name
);
Ok(())
}
@@ -688,8 +675,7 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
.await?;
for (id, public_key, private_key, is_default) in keys {
let id = Some(id).filter(|_| is_default == 0);
let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key {
if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await {
error!(context, "Failed to export public key: {:#}.", err);
@@ -880,35 +866,14 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_and_import_key() {
let export_dir = tempfile::tempdir().unwrap();
let context = TestContext::new_alice().await;
if let Err(err) = imex(
&context.ctx,
ImexMode::ExportSelfKeys,
export_dir.path(),
None,
)
.await
{
let blobdir = context.ctx.get_blobdir();
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await {
panic!("got error on export: {err:#}");
}
let context2 = TestContext::new_alice().await;
if let Err(err) = imex(
&context2.ctx,
ImexMode::ImportSelfKeys,
export_dir.path(),
None,
)
.await
{
panic!("got error on import: {err:#}");
}
let keyfile = export_dir.path().join("private-key-default.asc");
let context3 = TestContext::new_alice().await;
if let Err(err) = imex(&context3.ctx, ImexMode::ImportSelfKeys, &keyfile, None).await {
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await {
panic!("got error on import: {err:#}");
}
}

390
src/job.rs Normal file
View File

@@ -0,0 +1,390 @@
//! # Job module.
//!
//! This module implements a job queue maintained in the SQLite database
//! and job types.
#![allow(missing_docs)]
use std::fmt;
use std::sync::atomic::Ordering;
use anyhow::{Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
use crate::context::Context;
use crate::imap::Imap;
use crate::scheduler::InterruptInfo;
use crate::tools::time;
// results in ~3 weeks for the last backoff timespan
const JOB_RETRIES: u32 = 17;
/// Job try result.
#[derive(Debug, Display)]
pub enum Status {
Finished(Result<()>),
RetryNow,
}
#[macro_export]
macro_rules! job_try {
($expr:expr) => {
match $expr {
std::result::Result::Ok(val) => val,
std::result::Result::Err(err) => {
return $crate::job::Status::Finished(Err(err.into()));
}
}
};
($expr:expr,) => {
$crate::job_try!($expr)
};
}
#[derive(
Debug,
Display,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
)]
#[repr(u32)]
pub enum Action {
// This job will download partially downloaded messages completely
// and is added when download_full() is called.
// Most messages are downloaded automatically on fetch
// and do not go through this job.
DownloadMsg = 250,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Job {
pub job_id: u32,
pub action: Action,
pub foreign_id: u32,
pub desired_timestamp: i64,
pub added_timestamp: i64,
pub tries: u32,
}
impl fmt::Display for Job {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "#{}, action {}", self.job_id, self.action)
}
}
impl Job {
pub fn new(action: Action, foreign_id: u32) -> Self {
let timestamp = time();
Self {
job_id: 0,
action,
foreign_id,
desired_timestamp: timestamp,
added_timestamp: timestamp,
tries: 0,
}
}
/// Deletes the job from the database.
async fn delete(self, context: &Context) -> Result<()> {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", (self.job_id as i32,))
.await?;
}
Ok(())
}
/// Saves the job to the database, creating a new entry if necessary.
///
/// The Job is consumed by this method.
pub(crate) async fn save(self, context: &Context) -> Result<()> {
info!(context, "saving job {:?}", self);
if self.job_id != 0 {
context
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=? WHERE id=?;",
(
self.desired_timestamp,
i64::from(self.tries),
self.job_id as i32,
),
)
.await?;
} else {
context.sql.execute(
"INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?,?,?,?);",
(
self.added_timestamp,
self.action,
self.foreign_id,
self.desired_timestamp
)
).await?;
}
Ok(())
}
}
pub(crate) enum Connection<'a> {
Inbox(&'a mut Imap),
}
impl<'a> Connection<'a> {
fn inbox(&mut self) -> &mut Imap {
match self {
Connection::Inbox(imap) => imap,
}
}
}
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
info!(context, "Job {} started...", &job);
let try_res = match perform_job_action(context, &job, &mut connection, 0).await {
Status::RetryNow => perform_job_action(context, &job, &mut connection, 1).await,
x => x,
};
match try_res {
Status::RetryNow => {
let tries = job.tries + 1;
if tries < JOB_RETRIES {
info!(context, "Increase job {job} tries to {tries}.");
job.tries = tries;
let time_offset = get_backoff_time_offset(tries);
job.desired_timestamp = time() + time_offset;
info!(
context,
"job #{} not succeeded on try #{}, retry in {} seconds.",
job.job_id,
tries,
time_offset
);
job.save(context).await.unwrap_or_else(|err| {
error!(context, "Failed to save job: {err:#}.");
});
} else {
info!(
context,
"Remove job {job} as it exhausted {JOB_RETRIES} retries."
);
job.delete(context).await.unwrap_or_else(|err| {
error!(context, "Failed to delete job: {err:#}.");
});
}
}
Status::Finished(res) => {
if let Err(err) = res {
warn!(context, "Remove job {job} as it failed with error {err:#}.");
} else {
info!(context, "Remove job {job} as it succeeded.");
}
job.delete(context).await.unwrap_or_else(|err| {
error!(context, "failed to delete job: {:#}", err);
});
}
}
}
async fn perform_job_action(
context: &Context,
job: &Job,
connection: &mut Connection<'_>,
tries: u32,
) -> Status {
info!(context, "Begin immediate try {tries} of job {job}.");
let try_res = match job.action {
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
};
info!(context, "Finished immediate try {tries} of job {job}.");
try_res
}
fn get_backoff_time_offset(tries: u32) -> i64 {
// Exponential backoff
let n = 2_i32.pow(tries - 1) * 60;
let mut rng = thread_rng();
let r: i32 = rng.gen();
let mut seconds = r % (n + 1);
if seconds < 1 {
seconds = 1;
}
i64::from(seconds)
}
pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
context.resync_request.store(true, Ordering::Relaxed);
context
.scheduler
.interrupt_inbox(InterruptInfo {
probe_network: false,
})
.await;
Ok(())
}
/// Adds a job to the database, scheduling it.
pub async fn add(context: &Context, job: Job) -> Result<()> {
job.save(context).await.context("failed to save job")?;
info!(context, "Interrupt: IMAP.");
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
Ok(())
}
/// Load jobs from the database.
///
/// The `probe_network` parameter decides how to query
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
pub(crate) async fn load_next(context: &Context, info: &InterruptInfo) -> Result<Option<Job>> {
info!(context, "Loading job.");
let query;
let params;
let t = time();
if !info.probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
query = r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE desired_timestamp<=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
params = vec![t];
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
// in the order of their backoff-times.
query = r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE tries>0
ORDER BY desired_timestamp, action DESC
LIMIT 1;
"#;
params = vec![];
};
loop {
let job_res = context
.sql
.query_row_optional(query, rusqlite::params_from_iter(params.clone()), |row| {
let job = Job {
job_id: row.get("id")?,
action: row.get("action")?,
foreign_id: row.get("foreign_id")?,
desired_timestamp: row.get("desired_timestamp")?,
added_timestamp: row.get("added_timestamp")?,
tries: row.get("tries")?,
};
Ok(job)
})
.await;
match job_res {
Ok(job) => return Ok(job),
Err(err) => {
// Remove invalid job from the DB
info!(context, "Cleaning up job, because of {err:#}.");
// TODO: improve by only doing a single query
let id = context
.sql
.query_row(query, rusqlite::params_from_iter(params.clone()), |row| {
row.get::<_, i32>(0)
})
.await
.context("failed to retrieve invalid job ID from the database")?;
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", (id,))
.await
.with_context(|| format!("failed to delete invalid job {id}"))?;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
async fn insert_job(context: &Context, foreign_id: i64, valid: bool) {
let now = time();
context
.sql
.execute(
"INSERT INTO jobs
(added_timestamp, action, foreign_id, desired_timestamp)
VALUES (?, ?, ?, ?);",
(
now,
if valid {
Action::DownloadMsg as i32
} else {
-1
},
foreign_id,
now,
),
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_next_job_two() -> Result<()> {
// We want to ensure that loading jobs skips over jobs which
// fails to load from the database instead of failing to load
// all jobs.
let t = TestContext::new().await;
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
assert!(jobs.is_none());
insert_job(&t, 1, true).await;
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
assert!(jobs.is_some());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_next_job_one() -> Result<()> {
let t = TestContext::new().await;
insert_job(&t, 1, true).await;
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
assert!(jobs.is_some());
Ok(())
}
}

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