Compare commits

..

2 Commits

Author SHA1 Message Date
holger krekel
3617b95a9d test: allow to run the test suite against underscore-domain relays
Such relays serve self-signed certificates, and are created e.g. by cmlxc deploys.
2026-07-30 18:02:51 +02:00
holger krekel
30a196c483 test: load test data through the data fixture
Relative paths only worked when running from `deltachat-rpc-client`.
2026-03-01 23:51:44 +01:00
217 changed files with 8015 additions and 12538 deletions

View File

@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.98.1
RUST_VERSION: 1.97.1
# Minimum Supported Rust Version
MSRV: 1.89.0
@@ -40,7 +40,7 @@ jobs:
- run: rustup override set $RUST_VERSION
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
@@ -80,7 +80,7 @@ jobs:
show-progress: false
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
@@ -126,13 +126,13 @@ jobs:
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Install nextest
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc
uses: taiki-e/install-action@07b4745e0c39a41822af610387492e3e53aa222b
with:
tool: nextest
@@ -163,7 +163,7 @@ jobs:
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
@@ -192,7 +192,7 @@ jobs:
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
@@ -300,7 +300,7 @@ jobs:
path: target/debug
- name: Install python
uses: actions/setup-python@v7.0.0
uses: actions/setup-python@v6.3.0
with:
python-version: ${{ matrix.python }}
@@ -348,7 +348,7 @@ jobs:
persist-credentials: false
- name: Install python
uses: actions/setup-python@v7.0.0
uses: actions/setup-python@v6.3.0
with:
python-version: ${{ matrix.python }}

View File

@@ -370,14 +370,6 @@ jobs:
- name: List artifacts
run: ls -l dist/
- name: Check that the wheel metadata reads back
run: |
echo 'You can check a local `nix build` on non-Mac machines against the following checksums.'
sha256sum dist/*.whl
mkdir tagcheck
cp dist/*.whl tagcheck/
nix run --inputs-from . nixpkgs#python3Packages.wheel -- tags tagcheck/*.whl
- name: Upload binaries to the GitHub release
if: github.event_name == 'release'
env:
@@ -390,7 +382,7 @@ jobs:
- name: Publish deltachat-rpc-server to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
@@ -409,7 +401,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: actions/setup-python@v7.0.0
- uses: actions/setup-python@v6.3.0
with:
python-version: "3.11"

View File

@@ -31,15 +31,15 @@ jobs:
package-manager-cache: false # never use caching in release builds
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
- name: Package
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: |
npm run build
npm pack .
- name: Publish
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public

View File

@@ -25,21 +25,21 @@ jobs:
with:
node-version: 24
- name: Add Rust cache
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: npm install
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm install
- name: Build TypeScript, run Rust tests, generate bindings
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm run build
- name: Run integration tests
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
- name: Run linter
working-directory: deltachat-jsonrpc-bindings/typescript
working-directory: deltachat-jsonrpc/typescript
run: npm run prettier:check

View File

@@ -5,13 +5,11 @@ on:
paths:
- flake.nix
- flake.lock
- nix/**
- .github/workflows/nix.yml
push:
paths:
- flake.nix
- flake.lock
- nix/**
- .github/workflows/nix.yml
branches:
- main
@@ -28,7 +26,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- run: nix fmt flake.nix nix/ -- --check
- run: nix fmt flake.nix -- --check
build:
name: nix build

View File

@@ -47,4 +47,4 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b

View File

@@ -81,7 +81,7 @@ jobs:
defaults:
run:
working-directory: ./deltachat-jsonrpc-bindings/typescript
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v7
@@ -104,7 +104,7 @@ jobs:
mkdir -p "$HOME/.ssh"
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc-bindings/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
build-cffi:
runs-on: ubuntu-latest

View File

@@ -23,4 +23,4 @@ jobs:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0

View File

@@ -1,254 +1,5 @@
# Changelog
## [2.60.0] - 2026-09-11
### API-Changes
- [**breaking**] remove a relay immediately instead of unpublishing it.
- `set_transport_unpublished()` is removed: UIs call `delete_transport()` when the user removes a relay.
- `list_transports_ex()` and the `TransportListEntry` type are removed: use `list_transports()`.
- `delete_transport()` no longer refuses to remove the primary transport: it refuses only to remove the last one and re-elects the sending transport as needed.
- `TransportsModified` is now also emitted on the device modifying the transports, not only on devices applying the synced change.
- [**breaking**] do not load webxdc icon if it has too large dimensions.
- `get_webxdc_blob()` may fail to load `icon.png` or `icon.jpg` if image dimensions are too large.
Fixing the issue discovered by https://github.com/Sergei768
- Generate JSON-RPC headers at build time ([#8350](https://github.com/chatmail/core/pull/8350)).
- Generate Qt JSON-RPC bindings ([#8330](https://github.com/chatmail/core/pull/8330)).
### Features / Changes
- Introduce keyupdate messages informing contacts about relay changes.
- Remove `Final-Recipient` from MDNs (and keyupdates).
- Carry all published relay addresses in securejoin links ([#8591](https://github.com/chatmail/core/pull/8591)).
- Use display name for contacts in encryption info ([#8609](https://github.com/chatmail/core/pull/8609)).
- Do not create device messages for IMAP authentication errors.
- Delete avatars referred to by parameters of special contacts.
- Import `Autocrypt-Gossip` keys without checking the addresses.
- Ignore `Chat-Disposition-Notification-To` value.
- Increase `sys.msgsize_max_recommended` to match chatmail relay message size limit.
### Fixes
- Do not try to load profile image from param for self.
- Send legacy securejoin key requests as `multipart/mixed` so they are not rejected by chatmail relays.
- rpc: avoid hang when requests race a dying rpc-server.
- Take `timestamp_rcvd` into account in `estimate_deletion_cnt`.
- Reliably complete configuration with progress=1000 or progress=0.
- Make `create_send_msg_jobs` actually return row IDs.
- Don't notify of missed call from blocked user.
- Trash MDNs that reference no message early.
- Return no relay address for key-contacts without an address.
- Do not emit events in `set_profile_image()` if contact avatar is unchanged.
- Remove `Original-Recipient` field from MDNs.
- ffi: support custom allocators in event string getters.
- Start checking column documentation in CI and add comment for `transports.add_timestamp`.
- Sanitize `version_string` we got from the wire ([#8582](https://github.com/chatmail/core/pull/8582))
- RUSTSEC-2026-0258 ([#8603](https://github.com/chatmail/core/pull/8603)).
### Build system
- Use `--locked` in `scripts/clippy.sh`.
- Produce correct wheel metadata.
### Documentation
- Always suggest using `--locked` with "cargo install".
- JSON-RPC: clarify when `reactions` is `None`.
- Fix async-imap and async-smtp URLs in README.md ([#8637](https://github.com/chatmail/core/pull/8637)).
- Update the timeout value in `DC_EVENT_CALL_ENDED` description.
### Refactor
- Don't store email address in location KML. ([#8615](https://github.com/chatmail/core/pull/8615)).
- Turn `DC_CHAT_ID_*` into `ChatId::*` associated constants.
- Turn `DC_MSG_ID_*` into `MsgId::*` associated constants.
- Don't include email addresses in export filenames ([#8626](https://github.com/chatmail/core/pull/8626)).
- Make `create_send_msg_jobs()` private.
- Rename `automatic_relay_management` to autorelay.
- Extract shared pieces for non-chat messages.
- Remove unused functions from the tools module.
- Remove the code to set own avatar in `set_profile_image()`.
- Move pgp tests to submodule.
- Split `flake.nix` into multiple files.
- Use `&[..]` instead of `&Vec<..>`.
### Tests
- [**breaking**] rename rpc fixtures to disambiguate from ffi fixtures.
- Test `dc_send_msg_sync()`.
- Print which error/warning was expected if it does not arrive.
### CI
- Update Rust to 1.98.1.
### Miscellaneous Tasks
- Add script to show the sizes of futures (async Rust) ([#8536](https://github.com/chatmail/core/pull/8536)).
- deps: bump zizmorcore/zizmor-action from 0.6.1 to 0.6.2.
- deps: bump swatinem/rust-cache from 2.9.1 to 2.9.2.
- deps: bump pypa/gh-action-pypi-publish from 1.14.1 to 1.14.2.
- deps: bump taiki-e/install-action from 2.85.1 to 2.86.7.
- cargo: bump futures from 0.3.33 to 0.3.34.
- cargo: bump thiserror from 2.0.19 to 2.0.20.
- cargo: bump syn from 3.0.3 to 3.0.4.
- cargo: bump log from 0.4.33 to 0.4.34.
- cargo: bump mail-builder from 0.4.4 to 0.5.0.
- cargo: bump blake3 from 1.8.5 to 1.8.7.
- cargo: bump http-body-util from 0.1.3 to 0.1.5.
- cargo: bump uuid from 1.20.0 to 1.25.0.
- cargo: bump data-encoding from 2.11.0 to 2.11.1.
- bump chacha20 0.10.1 to 0.10.2.
## [2.59.0] - 2026-08-14
### API-Changes
- [**breaking**] Remove deprecated `dc_chat_is_protected()`.
- Deprecate `dc_chat_get_info_json()` ([#8580](https://github.com/chatmail/core/pull/8580))
- New `get_app_version()` JSON-RPC API to get information about available updates.
### Features / Changes
- Add stock strings for being added/removed from group ([#8562](https://github.com/chatmail/core/pull/8562)).
- Client version information ([#8557](https://github.com/chatmail/core/pull/8557)).
- Remove hidden headers.
- Stop creating info messages for old broadcast lists.
### Fixes
- Filtered reactions are info, not error in device chat.
- Send MDNs to self even if MDNs are disabled.
- Send HTTP requests in origin not absolute form.
### Documentation
- json-rpc: improve `reactions_by_contact` doc.
- Do not refer to `is_chat_protected()`.
- Do not talk about verified chats in securejoin QR-scanning functions.
- Add SQL schema documentation.
### Miscellaneous Tasks
- Fix nightly clippy warnings.
- cargo: bump astral-tokio-tar from 0.6.3 to 0.6.4.
- cargo: bump bytes from 1.12.0 to 1.12.1.
- FFI: don't swallow but log errors in three places.
### Refactor
- Remove `MessengerMessage`.
- Stop setting chats.protected column explicitly.
- Merge `msg_group_left_local` into `msg_del_member_local` ([#8575](https://github.com/chatmail/core/pull/8575)).
- Rename `_ex()` -> `_ext()`.
- mimefactory: add Encryption enum.
### Tests
- Fix flakyness of iroh tests by sending "forever" so that late swarm-joins still make the test work.
- Move iroh tests into separate module.
- Provide complete test isolation by not re-using account addresses.
- Avoid another source of random failures with `direct_imap` failing to connect on first try.
- Remove all cache-related logic in the FFI pytest plugin.
- Add a CI-failing check that documented sql schema matches real one.
- Abort early if DNS to chatmail domain does not work and nicer pytest startup header.
- Load test data through the `data` fixture.
- Allow to run the test suite against underscore-domain relays.
## [2.58.0] - 2026-08-10
### API-Changes
- [**breaking**] remove getPushState() and core's internal tracking of it
- [**breaking**] remove `dc_chatlist_get_context()`, because it was easy to misuse and likely led to crashes ([#8503](https://github.com/chatmail/core/pull/8503))
- instead, store reference-counted Context in `dc_msg_t`, `dc_contact_t` and `dc_chatlist_t`
- add "pinned messages" API.
### Build system
- update all crates to Rust 2024 edition.
### CI
- update github actions monthly instead of weekly.
### Documentation
- clarify `ChatId::do_set_draft()` docs.
- add missing slash to ConnectionSecurity::Starttls doc comment.
### Features / Changes
- send Autocrypt pgp key in MDNs occassionally and when relaylist changes.
- reduce unncessary gossipping of keys in group chats.
- stop requiring XDELTAPUSH capability for push notifications.
- prepare basic multi-relay onboarding ([#8444](https://github.com/chatmail/core/pull/8444))
- collect ICE servers from all relays.
- send messages to 5 relays instead of the newest 3 ones.
- allow to send reactions in broadcast channels ([#8450](https://github.com/chatmail/core/pull/8450)).
- allow only default reactions in channels broadcast ([#8545](https://github.com/chatmail/core/pull/8545)).
- resend pinned state in broadcast channels ([#8549](https://github.com/chatmail/core/pull/8549)).
### Fixes
- **The primary transport is not synchronized between devices anymore.**
- Don't warn about correct EXIF orientation values. ([#8483](https://github.com/chatmail/core/pull/8483)).
- deltachat-rpc-client: don't depend on execnet for importing pytest plugin, remove deprecated "py" usage.
- send MDNs to all authentic relays of a contact, not just whatever `get_addr()` returns..
- mark `as_path()` function unsafe.
- python: create event emitter when EventThread is initialized.
- Don't download pre-message again if it is known already ([#8488](https://github.com/chatmail/core/pull/8488)).
- recognize self addresses in various places (instead of just the "primary").
- fix multi relay connectivity view ([#8550](https://github.com/chatmail/core/pull/8550)).
- ensure same-second primary transport change propagates correctly.
- invalidate `configured_addr` cache before sending transport sync message.
- prevent transport de-synchronization because of early fetch cancellation.
- improve connectivity HTML if quota info has an error.
### Miscellaneous Tasks
- bump version to 2.58.0-dev.
- deps: bump actions/setup-python from 6 to 6.3.0.
- deps: bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0.
- cargo: bump futures from 0.3.32 to 0.3.33.
- cargo: bump tokio from 1.52.3 to 1.53.0.
- cargo: bump regex from 1.12.4 to 1.13.1.
- disable "large futures" lint again.
- cargo: bump tokio-util from 0.7.18 to 0.7.19.
- deps: bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1.
- deps: bump taiki-e/install-action from 2.83.4 to 2.85.1.
- cargo: bump `serde_json` from 1.0.150 to 1.0.151.
- deps: bump pypa/gh-action-pypi-publish from 1.14.0 to 1.14.1.
- deps: bump actions/setup-python from 6.3.0 to 7.0.0.
- cargo: introduce syn 3 dependency.
- cargo: bump anyhow from 1.0.103 to 1.0.104.
- cargo: bump serde from 1.0.228 to 1.0.229.
- cargo: bump thiserror from 2.0.18 to 2.0.19.
- cargo: bump libc from 0.2.186 to 0.2.189.
### Performance
- Box::pin iroh::endpoint::Builder::bind in order to reduce memory usage.
### Refactor
- use the new regex! macro.
- Remove FolderMeaning and `target_folder` ([#8456](https://github.com/chatmail/core/pull/8456)).
- Unify naming of direct/single/1:1/normal chats ([#8442](https://github.com/chatmail/core/pull/8442)).
- un-nest `prepare_msg_blob`.
- do not clean `imap_send` table on transport change.
- mark enabled ephemeral timer duration as NonZero.
- reduce the scope of unsafe in `dc_context_unref()`.
- mimefactory: separate rendering of message payload and sendable message.
### Tests
- fix flaky `test_markseen_message_and_mdn` test.
- fix flaky `test_no_markseen_in_team_profile` ([#8500](https://github.com/chatmail/core/pull/8500)).
- Add `test_bcc_self`.
- Add test for unencrypted headers ([#8538](https://github.com/chatmail/core/pull/8538)).
- Assert log warnings and errors ([#8457](https://github.com/chatmail/core/pull/8457)).
## [2.57.0] - 2026-07-25
### API-Changes
@@ -8791,6 +8542,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.55.0]: https://github.com/chatmail/core/compare/v2.54.0..v2.55.0
[2.56.0]: https://github.com/chatmail/core/compare/v2.55.0..v2.56.0
[2.57.0]: https://github.com/chatmail/core/compare/v2.56.0..v2.57.0
[2.58.0]: https://github.com/chatmail/core/compare/v2.57.0..v2.58.0
[2.59.0]: https://github.com/chatmail/core/compare/v2.58.0..v2.59.0
[2.60.0]: https://github.com/chatmail/core/compare/v2.59.0..v2.60.0

View File

@@ -2,59 +2,45 @@ cmake_minimum_required(VERSION 3.16)
project(deltachat LANGUAGES C)
include(GNUInstallDirs)
option(WITH_JSONRPC_BINDINGS "Generate jsonrpc bindings" OFF)
find_program(CARGO cargo)
if(APPLE)
set(DYNAMIC_EXT "dylib")
set(DYNAMIC_EXT "dylib")
elseif(UNIX)
set(DYNAMIC_EXT "so")
set(DYNAMIC_EXT "so")
else()
set(DYNAMIC_EXT "dll")
set(DYNAMIC_EXT "dll")
endif()
if(DEFINED ENV{CARGO_BUILD_TARGET})
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/$ENV{CARGO_BUILD_TARGET}/release")
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
else()
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/release")
set(ARCH_DIR "./")
endif()
if(WITH_JSONRPC_BINDINGS)
set(JSONRPC_ARGS --package deltachat-jsonrpc-bindings)
endif()
add_custom_target(
lib_deltachat
ALL
COMMAND
${CMAKE_COMMAND} -E env
CARGO_TARGET_DIR="${CMAKE_BINARY_DIR}/target"
PREFIX="${CMAKE_INSTALL_PREFIX}"
LIBDIR="${CMAKE_INSTALL_FULL_LIBDIR}"
INCLUDEDIR="${CMAKE_INSTALL_FULL_INCLUDEDIR}"
${CARGO} build --release --package deltachat_ffi ${JSONRPC_ARGS}
WORKING_DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}"
add_custom_command(
OUTPUT
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
install(FILES "${CARGO_OUT_DIR}/libdeltachat.a" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
install(FILES "${CARGO_OUT_DIR}/libdeltachat.${DYNAMIC_EXT}" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
install(FILES "${CARGO_OUT_DIR}/pkgconfig/deltachat.pc" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
add_custom_target(
lib_deltachat
ALL
DEPENDS
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
)
if(WITH_JSONRPC_BINDINGS)
install(
FILES
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/generated/types.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/generated/client.hpp"
DESTINATION
"${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc/generated"
)
install(
FILES
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp"
DESTINATION
"${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc"
)
endif()
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

232
Cargo.lock generated
View File

@@ -124,9 +124,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anyhow"
version = "1.0.104"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "argon2"
@@ -194,15 +194,15 @@ dependencies = [
[[package]]
name = "astral-tokio-tar"
version = "0.6.4"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463"
checksum = "08648fef353ab39a9d26f909ad53fc4f071be4c91853b78523f5cc3d9e5ebffd"
dependencies = [
"futures-core",
"libc",
"portable-atomic",
"rustc-hash",
"rustix 1.1.4",
"rustix 0.38.44",
"tokio",
"tokio-stream",
]
@@ -310,7 +310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37dd6b179962fe4048a6f81d4c0d7ed419a21fdf49204b4c6b04971693358e79"
dependencies = [
"native-tls",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"url",
]
@@ -327,7 +327,7 @@ dependencies = [
"log",
"nom 8.0.0",
"pin-project",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
]
@@ -363,7 +363,7 @@ dependencies = [
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tokio-util",
]
@@ -459,7 +459,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
"thiserror 2.0.20",
"thiserror 2.0.18",
]
[[package]]
@@ -497,10 +497,11 @@ dependencies = [
[[package]]
name = "blake3"
version = "1.8.7"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
@@ -697,9 +698,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.12.1"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
dependencies = [
"serde",
]
@@ -804,9 +805,9 @@ dependencies = [
[[package]]
name = "chacha20"
version = "0.10.2"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
@@ -1312,9 +1313,9 @@ dependencies = [
[[package]]
name = "data-encoding"
version = "2.11.1"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]]
name = "dbl"
@@ -1327,7 +1328,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.61.0-dev"
version = "2.58.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1399,7 +1400,7 @@ dependencies = [
"tempfile",
"testdir",
"textwrap",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tokio-io-timeout",
"tokio-rustls",
@@ -1435,7 +1436,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.61.0-dev"
version = "2.58.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1454,16 +1455,9 @@ dependencies = [
"yerpc",
]
[[package]]
name = "deltachat-jsonrpc-bindings"
version = "2.61.0-dev"
dependencies = [
"deltachat-jsonrpc",
]
[[package]]
name = "deltachat-repl"
version = "2.61.0-dev"
version = "2.58.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1479,7 +1473,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.61.0-dev"
version = "2.58.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1503,12 +1497,12 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 3.0.4",
"syn 2.0.118",
]
[[package]]
name = "deltachat_ffi"
version = "2.61.0-dev"
version = "2.58.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1518,7 +1512,7 @@ dependencies = [
"num-traits",
"rand 0.9.4",
"serde_json",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"yerpc",
]
@@ -1693,7 +1687,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.1",
"windows-sys 0.59.0",
]
[[package]]
@@ -2127,9 +2121,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
dependencies = [
"futures-channel",
"futures-core",
@@ -2155,9 +2149,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
dependencies = [
"futures-core",
"futures-sink",
@@ -2180,15 +2174,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
[[package]]
name = "futures-executor"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
dependencies = [
"futures-core",
"futures-task",
@@ -2197,9 +2191,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
[[package]]
name = "futures-lite"
@@ -2216,32 +2210,32 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 2.0.118",
]
[[package]]
name = "futures-sink"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
[[package]]
name = "futures-task"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-util"
version = "0.3.34"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-channel",
"futures-core",
@@ -2376,9 +2370,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.16"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
dependencies = [
"atomic-waker",
"bytes",
@@ -2465,7 +2459,7 @@ dependencies = [
"once_cell",
"rand 0.9.4",
"ring",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tinyvec",
"tokio",
"tracing",
@@ -2488,7 +2482,7 @@ dependencies = [
"rand 0.9.4",
"resolv-conf",
"smallvec",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tracing",
]
@@ -2587,9 +2581,9 @@ dependencies = [
[[package]]
name = "http-body-util"
version = "0.1.5"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
@@ -2706,7 +2700,7 @@ dependencies = [
"hyper",
"libc",
"pin-project-lite",
"socket2 0.6.3",
"socket2 0.5.9",
"tokio",
"tower-service",
"tracing",
@@ -3045,7 +3039,7 @@ dependencies = [
"strum 0.26.2",
"stun-rs",
"surge-ping",
"thiserror 2.0.20",
"thiserror 2.0.18",
"time",
"tokio",
"tokio-stream",
@@ -3070,7 +3064,7 @@ dependencies = [
"ed25519-dalek",
"rand_core 0.6.4",
"serde",
"thiserror 2.0.20",
"thiserror 2.0.18",
"url",
]
@@ -3112,7 +3106,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"serde-error",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tokio-util",
"tracing",
@@ -3157,7 +3151,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.5.9",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
@@ -3177,7 +3171,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
@@ -3232,7 +3226,7 @@ dependencies = [
"sha1",
"strum 0.26.2",
"stun-rs",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tokio-rustls",
"tokio-util",
@@ -3322,9 +3316,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.189"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libm"
@@ -3398,9 +3392,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.34"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "loom"
@@ -3444,9 +3438,9 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
name = "mail-builder"
version = "0.5.0"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
checksum = "900998f307338c4013a28ab14d760b784067324b164448c6d98a89e44810473b"
[[package]]
name = "mailparse"
@@ -3717,7 +3711,7 @@ dependencies = [
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 2.0.20",
"thiserror 2.0.18",
]
[[package]]
@@ -3842,7 +3836,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.1",
"windows-sys 0.59.0",
]
[[package]]
@@ -4206,7 +4200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror 2.0.20",
"thiserror 2.0.18",
"ucd-trie",
]
@@ -4382,7 +4376,7 @@ dependencies = [
"serde",
"sha1_smol",
"simple-dns",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tracing",
"url",
@@ -4780,7 +4774,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.5.9",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tracing",
]
@@ -4801,7 +4795,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
@@ -4901,7 +4895,7 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20 0.10.2",
"chacha20 0.10.1",
"getrandom 0.4.3",
"rand_core 0.10.1",
]
@@ -5040,7 +5034,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.20",
"thiserror 2.0.18",
]
[[package]]
@@ -5277,14 +5271,14 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.1",
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.23.45"
version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"brotli",
"brotli-decompressor",
@@ -5292,7 +5286,7 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.15",
"rustls-webpki 0.103.13",
"subtle",
"zeroize",
]
@@ -5329,9 +5323,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.15"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"ring",
"rustls-pki-types",
@@ -5527,9 +5521,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.229"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
@@ -5546,22 +5540,22 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.229"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 2.0.118",
]
[[package]]
@@ -5577,9 +5571,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.151"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
@@ -5717,7 +5711,7 @@ dependencies = [
"shadowsocks-crypto",
"socket2 0.5.9",
"spin 0.10.1",
"thiserror 2.0.20",
"thiserror 2.0.18",
"tokio",
"tokio-tfo",
"trait-variant",
@@ -6067,17 +6061,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.0"
@@ -6172,10 +6155,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.1",
"windows-sys 0.52.0",
]
[[package]]
@@ -6215,11 +6198,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.20"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl 2.0.20",
"thiserror-impl 2.0.18",
]
[[package]]
@@ -6235,13 +6218,13 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.20"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 2.0.118",
]
[[package]]
@@ -6400,16 +6383,15 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.19"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"futures-util",
"libc",
"pin-project-lite",
"tokio",
]
@@ -6763,11 +6745,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.25.0"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
dependencies = [
"getrandom 0.4.3",
"getrandom 0.3.3",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -7429,7 +7411,7 @@ dependencies = [
"futures",
"log",
"serde",
"thiserror 2.0.20",
"thiserror 2.0.18",
"windows 0.59.0",
"windows-core 0.59.0",
]
@@ -7535,9 +7517,9 @@ dependencies = [
[[package]]
name = "yerpc"
version = "0.7.0"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6924db1f1011f1d22566c45c7090aa66c3107c3c463ca32beaa8b2327127040a"
checksum = "1dc24983fbe850227bfc1de89bf8cbfb3e2463afc322e0de2f155c4c23d06445"
dependencies = [
"anyhow",
"async-channel 1.9.0",
@@ -7555,9 +7537,9 @@ dependencies = [
[[package]]
name = "yerpc_derive"
version = "0.7.0"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42c374248c189b15a7f0660db3984e9dcc790ec15f4b44e932e92be49de829f9"
checksum = "4d8560d021437420316370db865e44c000bf86380b47cf05e49be9d652042bf5"
dependencies = [
"convert_case",
"darling",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.61.0-dev"
version = "2.58.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.89"
@@ -70,7 +70,7 @@ iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.5.0", default-features = false }
mail-builder = { version = "0.4.4", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.17"
@@ -129,7 +129,6 @@ members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc",
"deltachat-jsonrpc-bindings",
"deltachat-rpc-server",
"deltachat-ratelimit",
"deltachat-repl",
@@ -203,7 +202,7 @@ thiserror = "2"
tokio = "1"
tokio-util = "0.7.18"
tracing-subscriber = "0.3"
yerpc = "0.7"
yerpc = "0.6.4"
[features]
default = ["vendored"]

View File

@@ -21,8 +21,8 @@ The following protocols are handled without requiring API users to know much abo
- secure TLS setup with DNS caching and shadowsocks/proxy support
- robust [SMTP](https://github.com/chatmail/async-smtp)
and [IMAP](https://github.com/chatmail/async-imap) handling
- robust [SMTP](https://github.com/chatmail/async-imap)
and [IMAP](https://github.com/chatmail/async-smtp) handling
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
and [MIME building](https://github.com/stalwartlabs/mail-builder).
@@ -167,7 +167,7 @@ $ cargo test -- --ignored
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
```sh
$ cargo install --locked cargo-bolero
$ cargo install cargo-bolero
```
Run fuzzing tests with

View File

@@ -1,8 +1,9 @@
[package]
name = "deltachat_ffi"
version = "2.61.0-dev"
version = "2.58.0-dev"
description = "Deltachat FFI"
edition = "2024"
readme = "README.md"
license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]

View File

@@ -461,6 +461,10 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `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.
* - `is_chatmail` = (deprecated) 1 if the the server is a chatmail server, 0 otherwise.
* This is deprecated, UIs should not behave differently
* for chatmail relays and classical email servers.
* Most usages in UIs can be replaced by `force_encryption`.
* - `is_muted` = Whether a context is muted by the user.
* Muted contexts should not sound, vibrate or show notifications.
* In contrast to `dc_set_chat_mute_duration()`,
@@ -522,10 +526,9 @@ int dc_set_config (dc_context_t* context, const char*
*
* - `sys.version` = get the version string e.g. as `1.2.3` or as `1.2.3special4`.
* - `sys.msgsize_max_recommended` = maximal recommended attachment size in bytes.
* All possible overheads are already subtracted and this value can be used
* e.g. for direct comparison with the size of a file the user wants to attach.
* If an attachment is larger than this value, the message is sent anyway,
* but email servers are likely to reject the message when receiving it or before trying to send.
* All possible overheads are already subtracted and this value can be used e.g. for direct comparison
* with the size of a file the user wants to attach. If an attachment is larger than this value,
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
* - `sys.config_keys` = get a space-separated list of all config-keys available.
* The config-keys are the keys that can be passed to the parameter `key` of this function.
*
@@ -597,10 +600,13 @@ char* dc_get_info (const dc_context_t* context);
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (4000): Show e.g. the string "Connected" or a green dot
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
@@ -693,21 +699,6 @@ char* dc_get_connectivity_html (dc_context_t* context);
void dc_configure (dc_context_t* context);
/**
* Add fake transport that cannot be used to connect.
*
* Used for offline tests only.
*
* To add a transport, use JSON-RPC calls `add_or_update_transport`
* and `add_transport_from_qr` instead.
*
* @memberof dc_context_t
* @param context The context object.
* @param addr The email address of the new transport.
*/
void dc_add_pseudo_transport (dc_context_t* context, const char *addr);
/**
* Check if the context is already configured.
*
@@ -1789,6 +1780,8 @@ int dc_is_contact_in_chat (dc_context_t* context, uint32_t ch
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* If the group has group protection enabled, only verified contacts can be added to the group.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* @memberof dc_context_t
@@ -2287,7 +2280,7 @@ void dc_block_contact (dc_context_t* context, uint32_t co
/**
* Get encryption info for a contact.
* Get a multi-line encryption info, containing your fingerprint and the
* fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
*
* @memberof dc_context_t
* @param context The context object.
@@ -2454,7 +2447,7 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
void dc_stop_ongoing_process (dc_context_t* context);
// securejoin
// out-of-band verification
#define DC_QR_ASK_VERIFYCONTACT 200 // id=contact
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
@@ -2489,7 +2482,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* The QR code state is returned in dc_lot_t::state as:
*
* - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID:
* ask whether to start chatting with the contact;
* ask whether to verify the contact;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
@@ -2498,7 +2491,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
* contact fingerprint matches,
* contact fingerprint verified,
* ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id().
*
@@ -2574,24 +2567,23 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer a SecureJoin invitation.
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if dc_check_qr() returns
* DC_QR_ASK_VERIFYCONTACT, DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
* the SecureJoin protocol can be started using dc_join_securejoin()
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* The returned text will also work as a normal https:-link,
* so that the QR code is useful also without Delta Chat being installed
* or can be passed to contacts through other channels.
*
* To reset invitations, pass the link to dc_set_config_from_qr().
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id If set to a group-chat-id,
* the SecureJoin QR code for the group is returned.
* If set to 0, the setup contact QR code is returned.
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://securejoin.delta.chat/
* for details about both protocols.
* @return The text that should go to the QR code,
@@ -2617,7 +2609,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_t chat_id);
/**
* Continue the SecureJoin protocol
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with dc_get_securejoin_qr().
* This function is typically called when dc_check_qr() returns
* lot.state=DC_QR_ASK_VERIFYCONTACT, lot.state=DC_QR_ASK_VERIFYGROUP or lot.state=DC_QR_ASK_VERIFYBROADCAST
@@ -2637,6 +2629,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
* to dc_check_qr().
* @return The chat ID of the joined chat, the UI may redirect to the this chat.
* On errors, 0 is returned, however, most errors will happen during handshake later on.
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
*/
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
@@ -3198,33 +3191,19 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
/**
* Perform a background fetch for all accounts in parallel with a timeout.
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
*
* For an account with IO stopped, the scheduler is paused
* and every transport is fetched concurrently on a dedicated connection.
* The account is done as soon as one transport received messages, the others stop.
* Only one batch of messages is fetched per transport this way,
* so a larger backlog is left to the next call or to started IO.
* dc_accounts_background_fetch() was created for the iOS Background fetch.
*
* For an account with IO running, IMAP IDLE is interrupted on every transport
* and the account is done once every transport is.
*
* The call never waits for outgoing messages and never triggers sending them itself.
* Received messages may still queue replies, securejoin handshakes for example,
* which go out only while IO is running.
*
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end,
* also on timeout, when another background fetch is already running
* and when the call is ignored because the timeout is too small,
* so it is safe to wait for the event whenever `accounts` is not NULL.
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end
* even in case of timeout, unless the function fails and returns 0.
* Process all events until you get this one and you can safely return to the background
* without forgetting to create a generic notification if no message was fetched.
* The event carries no data identifying the call it belongs to,
* so it marks your own call only if no concurrent background fetch is happening.
* without forgetting to create notifications caused by timing race conditions.
*
* @memberof dc_accounts_t
* @param accounts The account manager as created by dc_accounts_new().
* @param timeout The timeout in seconds
* @return Return 0 if the call was ignored because `accounts` is NULL or the timeout is too small, 1 otherwise.
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
*/
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
@@ -3580,9 +3559,17 @@ dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t ch
/**
* Get info summary for a chat, in JSON format.
* Helper function to get the associated context object.
*
* @deprecated 2026-08-13, use dedicated dc_chat_get_*() getters or jsonrpc
* @memberof dc_chatlist_t
* @param chatlist The chatlist object to empty.
* @return The context object associated with the chatlist. NULL if none or on errors.
*/
dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
/**
* Get info summary for a chat, in JSON format.
*
* The returned JSON string has the following key/values:
*
@@ -3594,6 +3581,7 @@ dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t ch
* last-message-state: @ref DC_STATE constant
* last-message-date:
* avatar-path: path-to-blobfile
* is_verified: yes/no
* @return a UTF8-encoded JSON string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
*/
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
@@ -3802,6 +3790,17 @@ int dc_chat_is_device_talk (const dc_chat_t* chat);
int dc_chat_can_send (const dc_chat_t* chat);
/**
* Deprecated, always returns 0.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return Always 0.
* @deprecated 2025-09-09
*/
int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Check if the chat is encrypted.
*
@@ -4484,7 +4483,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
* - DC_INFO_GROUP_DESCRIPTION_CHANGED (70) - Info-message "Description changed", UI should open the profile with the description
* - DC_INFO_MESSAGE_PINNED (71) - Message pinned, UI should scroll to the pinned message returned by dc_msg_get_parent()
*
* For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID.
@@ -4544,7 +4542,6 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
#define DC_INFO_CHAT_E2EE 50
#define DC_INFO_GROUP_DESCRIPTION_CHANGED 70
#define DC_INFO_MESSAGE_PINNED 71
/**
@@ -4862,8 +4859,6 @@ dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
* Used for Webxdc-info-messages
* to jump to the corresponding instance that created the info message.
*
* For Pinned-info-messages, this refers to the pinned message.
*
* For quotes, please use the more specialized
* dc_msg_get_quoted_text() and dc_msg_get_quoted_msg().
*
@@ -4903,20 +4898,6 @@ uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg);
*/
uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
/**
* Check if the message is pinned.
*
* Pinned messages should be marked by a pin needle in the UI.
* To pin messages or get all pinned messages, use jsonrpc's "setPinnedMessageState" and "getPinnedMessages".
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message is pinned, 0=message not pinned.
*/
int dc_msg_is_pinned (const dc_msg_t* msg);
/**
* @class dc_contact_t
*
@@ -5121,23 +5102,6 @@ int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
int dc_contact_was_seen_recently (const dc_contact_t* contact);
/**
* Check if the contact was not seen a long time ago.
*
* The UI shall highlight these contacts,
* draw a orange green dot on the avatars of the user,
* and show a hint in the contact's profile.
*
* DC_CONTACT_ID_SELF and other special contact IDs are defined as never been stale (they should not get a dot).
* To get the time a contact was seen, use dc_contact_get_last_seen().
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 1=contact seen recently, 0=contact not seen recently.
*/
int dc_contact_is_stale (const dc_contact_t* contact);
/**
* Check if a contact is blocked.
*
@@ -5150,6 +5114,19 @@ int dc_contact_is_stale (const dc_contact_t* contact);
int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if the contact
* can be added to protected chats.
*
* See dc_contact_get_verifier_id() for a guidance how to display these information.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions.
*/
int dc_contact_is_verified (dc_contact_t* contact);
/**
* Returns whether contact is a bot.
*
@@ -5174,6 +5151,36 @@ int dc_contact_is_bot (dc_contact_t* contact);
int dc_contact_is_key_contact (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
*
* As verifier may be unknown,
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
*
* UI should display the information in the contact's profile as follows:
*
* - If dc_contact_get_verifier_id() != 0,
* display text "Introduced by ..."
* with the name of the contact
* formatted by dc_contact_get_name().
* Prefix the text by a green checkmark.
*
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
* display "Introduced" prefixed by a green checkmark.
*
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
* display nothing
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
* we verified the contact ourself. If it is 0, we don't have verifier information or
* the contact is not verified.
*/
uint32_t dc_contact_get_verifier_id (dc_contact_t* contact);
/**
* @class dc_lot_t
*
@@ -5675,7 +5682,6 @@ void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
* - getAccountFileSize()
* - importVcard(), parseVcard(), makeVcard()
* - sendWebxdcRealtimeData, sendWebxdcRealtimeAdvertisement(), leaveWebxdcRealtime()
* - setPinnedMessageState(), getPinnedMessages()
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
@@ -6204,7 +6210,7 @@ void dc_event_unref(dc_event_t* event);
/**
* Contact(s) created, renamed, blocked or deleted.
* Contact(s) created, renamed, verified, blocked or deleted.
*
* @param data1 (int) contact_id of the changed contact or 0 on batch-changes or deletion.
* @param data2 0
@@ -6276,7 +6282,8 @@ void dc_event_unref(dc_event_t* event);
*
* @param data1 (int) The ID of the inviting contact.
* @param data2 (int) The progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
* (Bob has verified alice and waits until Alice does the same for him)
* 1000=vg-member-added/vc-contact-confirm received
*/
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
@@ -6359,18 +6366,11 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
/**
* Tells that a call to dc_accounts_background_fetch() is done:
* the fetch completed, timed out, was stopped or was not started.
*
* For the call that started the fetch, this event acts as a marker:
* when you reach it, all events emitted during the fetch were processed.
* A call made while another background fetch is running gets the event immediately,
* and the running fetch keeps emitting events until its own marker.
*
* The event carries no data identifying the call it belongs to,
* so it marks your own call only if no concurrent background fetch is happening.
* Your own call has finished when dc_accounts_background_fetch() returns.
* Tells that the Background fetch was completed (or timed out).
*
* This event acts as a marker, when you reach this event you can be sure
* that all events emitted during the background fetch were processed.
*
* This event is only emitted by the account manager
*/
@@ -6466,7 +6466,7 @@ void dc_event_unref(dc_event_t* event);
/**
* An incoming or outgoing call was ended using dc_end_call() on this or another device, by caller or callee.
* Moreover, the event is sent when the call was not accepted within two minutes.
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
*
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
@@ -6478,10 +6478,9 @@ void dc_event_unref(dc_event_t* event);
* Transport relay added/deleted or default has changed.
* UI should update the list.
*
* The event is emitted on the device modifying the transports
* as well as on other devices applying the synced change,
* for the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`,
* `delete_transport` or `set_config(configured_addr)`.
* The event is emitted when the transports are modified on another device
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`,
* `set_transport_unpublished` or `set_config(configured_addr)`.
*/
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
@@ -6666,12 +6665,21 @@ void dc_event_unref(dc_event_t* event);
/// Used to build the string returned by dc_get_contact_encrinfo().
#define DC_STR_FINGERPRINTS 30
/// "%1$s verified"
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the verified contact
#define DC_STR_CONTACT_VERIFIED 35
/// "Archived chats"
///
/// Used as the name for the corresponding chatlist entry.
#define DC_STR_ARCHIVEDCHATS 40
/// @deprecated 2026-08-24
/// "Cannot login as %1$s."
///
/// Used in error strings.
/// - %1$s will be replaced by the failing login name
#define DC_STR_CANNOT_LOGIN 60
/// "Location streaming enabled."
@@ -6844,7 +6852,7 @@ void dc_event_unref(dc_event_t* event);
///
/// Added as an info-message directly after scanning a QR code for joining a group.
/// May be followed by the info-messages
/// #DC_STR_SECURE_JOIN_REPLIES and #DC_STR_MSGADDMEMBER.
/// #DC_STR_SECURE_JOIN_REPLIES, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
///
/// `%1$s` and `%2$s` will be replaced by name of the inviter.
#define DC_STR_SECURE_JOIN_STARTED 117
@@ -6853,13 +6861,15 @@ void dc_event_unref(dc_event_t* event);
///
/// Info-message on scanning a QR code for joining a group.
/// Added after #DC_STR_SECURE_JOIN_STARTED.
/// If the handshake allows to skip a step and go for #DC_STR_CONTACT_VERIFIED directly,
/// this info-message is skipped.
///
/// `%1$s` will be replaced by the name of the inviter.
#define DC_STR_SECURE_JOIN_REPLIES 118
/// "Scan to chat with %1$s"
///
/// Subtitle for the invite qrcode svg image generated by the core.
/// Subtitle for verification qrcode svg image generated by the core.
///
/// `%1$s` will be replaced by name of the inviter.
#define DC_STR_SETUP_CONTACT_QR_DESC 119
@@ -7154,30 +7164,6 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by name of the removed contact.
#define DC_STR_REMOVE_MEMBER 178
/// "You were removed by %1$s."
///
/// `%1$s` will be replaced by name of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_REMOVE_YOU_BY 179
/// "You were added by %1$s."
///
/// `%1$s` will be replaced by name of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_ADD_YOU_BY 180
/// "You were removed."
///
/// Used in status messages.
#define DC_STR_REMOVE_YOU 181
/// "You were added."
///
/// Used in status messages.
#define DC_STR_ADD_YOU 182
/// "Establishing connection, please wait…"
///
/// Used as info message.
@@ -7273,15 +7259,6 @@ void dc_event_unref(dc_event_t* event);
/// Used when creating text for the "Encryption Info" dialogs.
#define DC_STR_MESSAGES_ARE_E2EE 242
/// "You pinned a message."
#define DC_STR_MESSAGE_PINNED_BY_YOU 243
/// "Message pinned by %1$s."
#define DC_STR_MESSAGE_PINNED_BY_OTHER 244
/// @deprecated 2026-08-31
#define DC_STR_PHASING_OUT 245
/**
* @}
*/

View File

@@ -1,4 +1,5 @@
use crate::chat::ChatItem;
use crate::constants::DC_MSG_ID_DAYMARKER;
use crate::contact::ContactId;
use crate::location::Location;
use crate::message::MsgId;
@@ -20,7 +21,7 @@ impl dc_array_t {
Self::ContactIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
},
Self::Locations(array) => array[index].location_id,
Self::Uint(array) => array[index],

View File

@@ -23,6 +23,7 @@ use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::{Context, ContextBuilder};
use deltachat::ephemeral::Timer as EphemeralTimer;
@@ -31,7 +32,6 @@ use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
use deltachat::stock_str::StockMessage;
use deltachat::transport::add_pseudo_transport;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -194,11 +194,13 @@ pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc:
/// This function releases the memory of the `dc_context_t` structure.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_context_unref(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_unref()");
return;
unsafe {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_unref()");
return;
}
drop(Box::from_raw(context));
}
drop(unsafe { Box::from_raw(context) });
}
#[unsafe(no_mangle)]
@@ -415,21 +417,6 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
spawn_configure(ctx.clone());
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_add_pseudo_transport(
context: *mut dc_context_t,
addr: *const libc::c_char,
) {
if context.is_null() {
eprintln!("ignoring careless call to dc_add_pseudo_transport()");
return;
}
let ctx = unsafe { &*context };
let addr = to_string_lossy(addr);
block_on(add_pseudo_transport(ctx, &addr)).log_err(ctx).ok();
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -706,7 +693,7 @@ pub unsafe extern "C" fn dc_event_get_data1_str(event: *mut dc_event_t) -> *mut
match event {
EventType::IncomingWebxdcNotify { href, .. } => {
if let Some(href) = href {
href.strdup()
href.to_c_string().unwrap_or_default().into_raw()
} else {
ptr::null_mut()
}
@@ -735,7 +722,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::DeletedBlobFile(msg)
| EventType::Warning(msg)
| EventType::Error(msg)
| EventType::ErrorSelfNotInGroup(msg) => msg.strdup(),
| EventType::ErrorSelfNotInGroup(msg) => {
let data2 = msg.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::MsgsChanged { .. }
| EventType::ReactionsChanged { .. }
| EventType::IncomingMsg { .. }
@@ -769,27 +759,45 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::TransportsModified => ptr::null_mut(),
EventType::IncomingCall {
place_call_info, ..
} => place_call_info.strdup(),
} => {
let data2 = place_call_info.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::OutgoingCallAccepted {
accept_call_info, ..
} => accept_call_info.strdup(),
} => {
let data2 = accept_call_info.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.strdup()
comment.to_c_string().unwrap_or_default().into_raw()
} else {
ptr::null_mut()
}
}
EventType::ImexFileWritten(file) => file.strdup(),
EventType::ConfigSynced { key } => key.to_string().strdup(),
EventType::ImexFileWritten(file) => {
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::ConfigSynced { key } => {
let data2 = key.to_string().to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::WebxdcRealtimeData { data, .. } => {
let ptr = unsafe { libc::malloc(data.len()) };
unsafe { libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len()) };
ptr as *mut libc::c_char
}
EventType::IncomingReaction { reaction, .. } => reaction.as_str().strdup(),
EventType::IncomingWebxdcNotify { text, .. } => text.strdup(),
EventType::IncomingReaction { reaction, .. } => reaction
.as_str()
.to_c_string()
.unwrap_or_default()
.into_raw(),
EventType::IncomingWebxdcNotify { text, .. } => {
text.to_c_string().unwrap_or_default().into_raw()
}
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -901,7 +909,7 @@ pub unsafe extern "C" fn dc_get_chatlist(
eprintln!("ignoring careless call to dc_get_chatlist()");
return ptr::null_mut();
}
let context = unsafe { &*context };
let ctx = unsafe { &*context };
let qs = to_opt_string_lossy(query_str);
let qi = if query_id == 0 {
@@ -911,19 +919,16 @@ pub unsafe extern "C" fn dc_get_chatlist(
};
match block_on(chatlist::Chatlist::try_load(
context,
ctx,
flags as usize,
qs.as_deref(),
qi,
))
.context("Failed to get chatlist")
.log_err(context)
.log_err(ctx)
{
Ok(list) => {
let ffi_list = ChatlistWrapper {
context: context.clone(),
list,
};
let ffi_list = ChatlistWrapper { context, list };
Box::into_raw(Box::new(ffi_list))
}
Err(_) => ptr::null_mut(),
@@ -1059,7 +1064,7 @@ pub unsafe extern "C" fn dc_send_delete_request(
let ctx = unsafe { &*context };
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
block_on(message::delete_msgs_ext(ctx, &msg_ids, true))
block_on(message::delete_msgs_ex(ctx, &msg_ids, true))
.context("failed dc_send_delete_request() call")
.log_err(ctx)
.ok();
@@ -1100,7 +1105,7 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
MsgId::new(msg_id),
StatusUpdateSerial::new(last_known_serial),
))
.unwrap_or_log_default(ctx, "Failed to get webxdc status updates")
.unwrap_or_else(|_| "".to_string())
.strdup()
}
@@ -1211,7 +1216,7 @@ pub unsafe extern "C" fn dc_set_draft(
let msg = if msg.is_null() {
None
} else {
let ffi_msg = unsafe { &mut *msg };
let ffi_msg: &mut MessageWrapper = unsafe { &mut *msg };
Some(&mut ffi_msg.message)
};
@@ -1233,7 +1238,7 @@ pub unsafe extern "C" fn dc_add_device_msg(
let msg = if msg.is_null() {
None
} else {
let ffi_msg = unsafe { &mut *msg };
let ffi_msg: &mut MessageWrapper = unsafe { &mut *msg };
Some(&mut ffi_msg.message)
};
@@ -1270,15 +1275,15 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
eprintln!("ignoring careless call to dc_get_draft()");
return ptr::null_mut(); // NULL explicitly defined as "no draft"
}
let context = unsafe { &*context };
let ctx = unsafe { &*context };
match block_on(ChatId::new(chat_id).get_draft(context))
match block_on(ChatId::new(chat_id).get_draft(ctx))
.with_context(|| format!("Failed to get draft for chat #{chat_id}"))
.unwrap_or_default()
{
Some(draft) => {
let ffi_msg = MessageWrapper {
context: context.clone(),
context,
message: draft,
};
Box::into_raw(Box::new(ffi_msg))
@@ -1302,7 +1307,7 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
let add_daymarker = (flags & DC_GCM_ADDDAYMARKER) != 0;
Box::into_raw(Box::new(
block_on(chat::get_chat_msgs_ext(
block_on(chat::get_chat_msgs_ex(
ctx,
ChatId::new(chat_id),
MessageListOptions { add_daymarker },
@@ -1348,18 +1353,15 @@ pub unsafe extern "C" fn dc_get_similar_chatlist(
eprintln!("ignoring careless call to dc_get_similar_chatlist()");
return ptr::null_mut();
}
let context = unsafe { &*context };
let ctx = unsafe { &*context };
let chat_id = ChatId::new(chat_id);
match block_on(chat_id.get_similar_chatlist(context))
match block_on(chat_id.get_similar_chatlist(ctx))
.context("failed to get similar chatlist")
.log_err(context)
.log_err(ctx)
{
Ok(list) => {
let ffi_list = ChatlistWrapper {
context: context.clone(),
list,
};
let ffi_list = ChatlistWrapper { context, list };
Box::into_raw(Box::new(ffi_list))
}
Err(_) => ptr::null_mut(),
@@ -1768,7 +1770,8 @@ pub unsafe extern "C" fn dc_set_chat_name(
chat_id: u32,
name: *const libc::c_char,
) -> libc::c_int {
if context.is_null() || chat_id <= ChatId::LAST_SPECIAL.to_u32() || name.is_null() {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || name.is_null()
{
eprintln!("ignoring careless call to dc_set_chat_name()");
return 0;
}
@@ -1789,7 +1792,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
chat_id: u32,
image: *const libc::c_char,
) -> libc::c_int {
if context.is_null() || chat_id <= ChatId::LAST_SPECIAL.to_u32() {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() {
eprintln!("ignoring careless call to dc_set_chat_profile_image()");
return 0;
}
@@ -1951,7 +1954,7 @@ pub unsafe extern "C" fn dc_forward_msgs(
if context.is_null()
|| msg_ids.is_null()
|| msg_cnt <= 0
|| chat_id <= ChatId::LAST_SPECIAL.to_u32()
|| chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32()
{
eprintln!("ignoring careless call to dc_forward_msgs()");
return;
@@ -2024,15 +2027,15 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
eprintln!("ignoring careless call to dc_get_msg()");
return ptr::null_mut();
}
let context = unsafe { &*context };
let ctx = unsafe { &*context };
let message = match block_on(message::Message::load_from_db(context, MsgId::new(msg_id)))
let message = match block_on(message::Message::load_from_db(ctx, MsgId::new(msg_id)))
.with_context(|| format!("dc_get_msg could not rectieve msg_id {msg_id}"))
.log_err(context)
.log_err(ctx)
{
Ok(msg) => msg,
Err(_) => {
if MsgId::new(msg_id).is_special() {
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
// C-core API returns empty messages, do the same
message::Message::new(Viewtype::default())
} else {
@@ -2040,10 +2043,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
}
}
};
let ffi_msg = MessageWrapper {
context: context.clone(),
message,
};
let ffi_msg = MessageWrapper { context, message };
Box::into_raw(Box::new(ffi_msg))
}
@@ -2285,17 +2285,12 @@ pub unsafe extern "C" fn dc_get_contact(
eprintln!("ignoring careless call to dc_get_contact()");
return ptr::null_mut();
}
let context = unsafe { &*context };
let ctx = unsafe { &*context };
block_on(async move {
Contact::get_by_id(context, ContactId::new(contact_id))
Contact::get_by_id(ctx, ContactId::new(contact_id))
.await
.map(|contact| {
Box::into_raw(Box::new(ContactWrapper {
context: context.clone(),
contact,
}))
})
.map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact })))
.unwrap_or_else(|_| ptr::null_mut())
})
}
@@ -2403,7 +2398,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
};
block_on(securejoin::get_securejoin_qr(ctx, chat_id))
.unwrap_or_log_default(ctx, "Failed to generate securejoin QR code")
.unwrap_or_else(|_| "".to_string())
.strdup()
}
@@ -2413,7 +2408,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr_svg(
chat_id: u32,
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_securejoin_qr_svg()");
eprintln!("ignoring careless call to generate_verification_qr()");
return "".strdup();
}
let ctx = unsafe { &*context };
@@ -2424,7 +2419,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr_svg(
};
block_on(get_securejoin_qr_svg(ctx, chat_id))
.unwrap_or_log_default(ctx, "Failed to generate securejoin QR code SVG")
.unwrap_or_else(|_| "".to_string())
.strdup()
}
@@ -2455,7 +2450,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
chat_id: u32,
seconds: libc::c_int,
) {
if context.is_null() || chat_id <= ChatId::LAST_SPECIAL.to_u32() || seconds < 0 {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || seconds < 0 {
eprintln!("ignoring careless call to dc_send_locations_to_chat()");
return;
}
@@ -2747,7 +2742,7 @@ pub unsafe extern "C" fn dc_array_is_independent(
/// context, but the Rust API does not, so the FFI layer needs to glue
/// these together.
pub struct ChatlistWrapper {
context: Context,
context: *const dc_context_t,
list: chatlist::Chatlist,
}
@@ -2783,11 +2778,12 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
return 0;
}
let ffi_list = unsafe { &*chatlist };
let ctx = unsafe { &*ffi_list.context };
match ffi_list
.list
.get_chat_id(index)
.context("get_chat_id failed")
.log_err(&ffi_list.context)
.log_err(ctx)
{
Ok(chat_id) => chat_id.to_u32(),
Err(_) => 0,
@@ -2804,11 +2800,12 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id(
return 0;
}
let ffi_list = unsafe { &*chatlist };
let ctx = unsafe { &*ffi_list.context };
match ffi_list
.list
.get_msg_id(index)
.context("get_msg_id failed")
.log_err(&ffi_list.context)
.log_err(ctx)
{
Ok(msg_id) => msg_id.map_or(0, |msg_id| msg_id.to_u32()),
Err(_) => 0,
@@ -2832,15 +2829,12 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
Some(&ffi_chat.chat)
};
let ffi_list = unsafe { &*chatlist };
let ctx = unsafe { &*ffi_list.context };
let summary = block_on(
ffi_list
.list
.get_summary(&ffi_list.context, index, maybe_chat),
)
.context("get_summary failed")
.log_err(&ffi_list.context)
.unwrap_or_default();
let summary = block_on(ffi_list.list.get_summary(ctx, index, maybe_chat))
.context("get_summary failed")
.log_err(ctx)
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
@@ -2872,6 +2866,18 @@ pub unsafe extern "C" fn dc_chatlist_get_summary2(
Box::into_raw(Box::new(summary.into()))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_chatlist_get_context(
chatlist: *mut dc_chatlist_t,
) -> *const dc_context_t {
if chatlist.is_null() {
eprintln!("ignoring careless call to dc_chatlist_get_context()");
return ptr::null_mut();
}
let ffi_list = unsafe { &*chatlist };
ffi_list.context
}
// dc_chat_t
/// FFI struct for [dc_chat_t]
@@ -3039,6 +3045,11 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
.unwrap_or_default() as libc::c_int
}
#[unsafe(no_mangle)]
pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int {
0
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
@@ -3130,7 +3141,7 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
/// context, but the Rust API does not, so the FFI layer needs to glue
/// these together.
pub struct MessageWrapper {
context: Context,
context: *const dc_context_t,
message: message::Message,
}
@@ -3148,7 +3159,7 @@ pub unsafe extern "C" fn dc_msg_new(
let context = unsafe { &*context };
let viewtype = from_prim(viewtype).expect(&format!("invalid viewtype = {viewtype}"));
let msg = MessageWrapper {
context: context.clone(),
context,
message: message::Message::new(viewtype),
};
Box::into_raw(Box::new(msg))
@@ -3285,9 +3296,10 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
return "".strdup();
}
let ffi_msg = unsafe { &*msg };
let ctx = unsafe { &*ffi_msg.context };
ffi_msg
.message
.get_file(&ffi_msg.context)
.get_file(ctx)
.map(|p| p.to_string_lossy().strdup())
.unwrap_or_else(|| "".strdup())
}
@@ -3302,17 +3314,18 @@ pub unsafe extern "C" fn dc_msg_save_file(
return 0;
}
let ffi_msg = unsafe { &*msg };
let ctx = unsafe { &*ffi_msg.context };
let path = to_string_lossy(path);
let r = block_on(
ffi_msg
.message
.save_file(&ffi_msg.context, &std::path::PathBuf::from(path)),
.save_file(ctx, &std::path::PathBuf::from(path)),
);
match r {
Ok(()) => 1,
Err(_) => {
r.context("Failed to save file from message")
.log_err(&ffi_msg.context)
.log_err(ctx)
.unwrap_or_default();
0
}
@@ -3340,11 +3353,13 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_blob(
return ptr::null_mut();
}
let ffi_msg = unsafe { &*msg };
let blob = block_on(
let ctx = unsafe { &*ffi_msg.context };
let blob = block_on(async move {
ffi_msg
.message
.get_webxdc_blob(&ffi_msg.context, &to_string_lossy(filename)),
);
.get_webxdc_blob(ctx, &to_string_lossy(filename))
.await
});
match blob {
Ok(blob) => unsafe {
*ret_bytes = blob.len();
@@ -3366,18 +3381,16 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc
return "".strdup();
}
let ffi_msg = unsafe { &*msg };
let ctx = unsafe { &*ffi_msg.context };
let Ok(info) = block_on(ffi_msg.message.get_webxdc_info(&ffi_msg.context))
let Ok(info) = block_on(ffi_msg.message.get_webxdc_info(ctx))
.context("dc_msg_get_webxdc_info() failed to get info")
.log_err(&ffi_msg.context)
.log_err(ctx)
else {
return "".strdup();
};
serde_json::to_string(&info)
.unwrap_or_log_default(
&ffi_msg.context,
"dc_msg_get_webxdc_info() failed to serialise to json",
)
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
.strdup()
}
@@ -3402,9 +3415,10 @@ pub unsafe extern "C" fn dc_msg_get_filebytes(msg: *mut dc_msg_t) -> u64 {
return 0;
}
let ffi_msg = unsafe { &*msg };
let ctx = unsafe { &*ffi_msg.context };
block_on(ffi_msg.message.get_filebytes(&ffi_msg.context))
.unwrap_or_log_default(&ffi_msg.context, "Cannot get file size")
block_on(ffi_msg.message.get_filebytes(ctx))
.unwrap_or_log_default(ctx, "Cannot get file size")
.unwrap_or_default()
}
@@ -3494,10 +3508,11 @@ pub unsafe extern "C" fn dc_msg_get_summary(
Some(&ffi_chat.chat)
};
let ffi_msg = unsafe { &mut *msg };
let ctx = unsafe { &*ffi_msg.context };
let summary = block_on(ffi_msg.message.get_summary(&ffi_msg.context, maybe_chat))
let summary = block_on(ffi_msg.message.get_summary(ctx, maybe_chat))
.context("dc_msg_get_summary failed")
.log_err(&ffi_msg.context)
.log_err(ctx)
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
@@ -3512,10 +3527,11 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
return "".strdup();
}
let ffi_msg = unsafe { &mut *msg };
let ctx = unsafe { &*ffi_msg.context };
let summary = block_on(ffi_msg.message.get_summary(&ffi_msg.context, None))
let summary = block_on(ffi_msg.message.get_summary(ctx, None))
.context("dc_msg_get_summarytext failed")
.log_err(&ffi_msg.context)
.log_err(ctx)
.unwrap_or_default();
match usize::try_from(approx_characters) {
Ok(chars) => summary.truncated_text(chars).strdup(),
@@ -3611,7 +3627,8 @@ pub unsafe extern "C" fn dc_msg_get_info_contact_id(msg: *mut dc_msg_t) -> u32 {
return 0;
}
let ffi_msg = unsafe { &*msg };
block_on(ffi_msg.message.get_info_contact_id(&ffi_msg.context))
let context = unsafe { &*ffi_msg.context };
block_on(ffi_msg.message.get_info_contact_id(context))
.unwrap_or_default()
.map(|id| id.to_u32())
.unwrap_or_default()
@@ -3695,17 +3712,18 @@ pub unsafe extern "C" fn dc_msg_set_file_and_deduplicate(
return;
}
let ffi_msg = unsafe { &mut *msg };
let ctx = unsafe { &*ffi_msg.context };
ffi_msg
.message
.set_file_and_deduplicate(
&ffi_msg.context,
ctx,
unsafe { as_path(file) },
to_opt_string_lossy(name).as_deref(),
to_opt_string_lossy(filemime).as_deref(),
)
.context("Failed to set file")
.log_err(&ffi_msg.context)
.log_err(ctx)
.ok();
}
@@ -3759,14 +3777,15 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize(
return;
}
let ffi_msg = unsafe { &mut *msg };
let ctx = unsafe { &*ffi_msg.context };
block_on({
ffi_msg
.message
.latefiling_mediasize(&ffi_msg.context, width, height, duration)
.latefiling_mediasize(ctx, width, height, duration)
})
.context("Cannot set media size")
.log_err(&ffi_msg.context)
.log_err(ctx)
.ok();
}
@@ -3794,16 +3813,17 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
None
} else {
let ffi_quote = unsafe { &*quote };
if ffi_msg.context.get_id() != ffi_quote.context.get_id() {
if ffi_msg.context != ffi_quote.context {
eprintln!("ignoring attempt to quote message from a different context");
return;
}
Some(&ffi_quote.message)
};
block_on(ffi_msg.message.set_quote(&ffi_msg.context, quote_msg))
let context = unsafe { &*ffi_msg.context };
block_on(ffi_msg.message.set_quote(context, quote_msg))
.context("failed to set quote")
.log_err(&ffi_msg.context)
.log_err(context)
.ok();
}
@@ -3813,7 +3833,7 @@ pub unsafe extern "C" fn dc_msg_get_quoted_text(msg: *const dc_msg_t) -> *mut li
eprintln!("ignoring careless call to dc_msg_get_quoted_text()");
return ptr::null_mut();
}
let ffi_msg = unsafe { &*msg };
let ffi_msg: &MessageWrapper = unsafe { &*msg };
ffi_msg
.message
.quoted_text()
@@ -3826,17 +3846,15 @@ pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_
eprintln!("ignoring careless call to dc_get_quoted_msg()");
return ptr::null_mut();
}
let ffi_msg = unsafe { &*msg };
let res = block_on(ffi_msg.message.quoted_message(&ffi_msg.context))
let ffi_msg: &MessageWrapper = unsafe { &*msg };
let context = unsafe { &*ffi_msg.context };
let res = block_on(ffi_msg.message.quoted_message(context))
.context("failed to get quoted message")
.log_err(&ffi_msg.context)
.log_err(context)
.unwrap_or(None);
match res {
Some(message) => Box::into_raw(Box::new(MessageWrapper {
context: ffi_msg.context.clone(),
message,
})),
Some(message) => Box::into_raw(Box::new(MessageWrapper { context, message })),
None => ptr::null_mut(),
}
}
@@ -3847,17 +3865,15 @@ pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_
eprintln!("ignoring careless call to dc_msg_get_parent()");
return ptr::null_mut();
}
let ffi_msg = unsafe { &*msg };
let res = block_on(ffi_msg.message.parent(&ffi_msg.context))
let ffi_msg: &MessageWrapper = unsafe { &*msg };
let context = unsafe { &*ffi_msg.context };
let res = block_on(ffi_msg.message.parent(context))
.context("failed to get parent message")
.log_err(&ffi_msg.context)
.log_err(context)
.unwrap_or(None);
match res {
Some(message) => Box::into_raw(Box::new(MessageWrapper {
context: ffi_msg.context.clone(),
message,
})),
Some(message) => Box::into_raw(Box::new(MessageWrapper { context, message })),
None => ptr::null_mut(),
}
}
@@ -3868,10 +3884,11 @@ pub unsafe extern "C" fn dc_msg_get_original_msg_id(msg: *const dc_msg_t) -> u32
eprintln!("ignoring careless call to dc_msg_get_original_msg_id()");
return 0;
}
let ffi_msg = unsafe { &*msg };
block_on(ffi_msg.message.get_original_msg_id(&ffi_msg.context))
let ffi_msg: &MessageWrapper = unsafe { &*msg };
let context = unsafe { &*ffi_msg.context };
block_on(ffi_msg.message.get_original_msg_id(context))
.context("failed to get original message")
.log_err(&ffi_msg.context)
.log_err(context)
.unwrap_or_default()
.map(|id| id.to_u32())
.unwrap_or(0)
@@ -3883,25 +3900,16 @@ pub unsafe extern "C" fn dc_msg_get_saved_msg_id(msg: *const dc_msg_t) -> u32 {
eprintln!("ignoring careless call to dc_msg_get_saved_msg_id()");
return 0;
}
let ffi_msg = unsafe { &*msg };
block_on(ffi_msg.message.get_saved_msg_id(&ffi_msg.context))
let ffi_msg: &MessageWrapper = unsafe { &*msg };
let context = unsafe { &*ffi_msg.context };
block_on(ffi_msg.message.get_saved_msg_id(context))
.context("failed to get original message")
.log_err(&ffi_msg.context)
.log_err(context)
.unwrap_or_default()
.map(|id| id.to_u32())
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_msg_is_pinned(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_is_pinned()");
return 0;
}
let ffi_msg = unsafe { &*msg };
ffi_msg.message.is_pinned().into()
}
// dc_contact_t
/// FFI struct for [dc_contact_t]
@@ -3912,7 +3920,7 @@ pub unsafe extern "C" fn dc_msg_is_pinned(msg: *mut dc_msg_t) -> libc::c_int {
/// context, but the Rust API does not, so the FFI layer needs to glue
/// these together.
pub struct ContactWrapper {
context: Context,
context: *const dc_context_t,
contact: contact::Contact,
}
@@ -4000,9 +4008,10 @@ pub unsafe extern "C" fn dc_contact_get_profile_image(
return ptr::null_mut(); // NULL explicitly defined as "no profile image"
}
let ffi_contact = unsafe { &*contact };
let ctx = unsafe { &*ffi_contact.context };
block_on(ffi_contact.contact.get_profile_image(&ffi_contact.context))
.unwrap_or_log_default(&ffi_contact.context, "failed to get profile image")
block_on(ffi_contact.contact.get_profile_image(ctx))
.unwrap_or_log_default(ctx, "failed to get profile image")
.map(|p| p.to_string_lossy().strdup())
.unwrap_or_else(std::ptr::null_mut)
}
@@ -4014,14 +4023,15 @@ pub unsafe extern "C" fn dc_contact_get_color(contact: *mut dc_contact_t) -> u32
return 0;
}
let ffi_contact = unsafe { &*contact };
let ctx = unsafe { &*ffi_contact.context };
block_on(
ffi_contact
.contact
// We don't want any UIs displaying gray self-color.
.get_or_gen_color(&ffi_contact.context),
.get_or_gen_color(ctx),
)
.context("Contact::get_color()")
.log_err(&ffi_contact.context)
.log_err(ctx)
.unwrap_or(0)
}
@@ -4055,16 +4065,6 @@ pub unsafe extern "C" fn dc_contact_was_seen_recently(contact: *mut dc_contact_t
ffi_contact.contact.was_seen_recently() as libc::c_int
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_contact_is_stale(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_is_stale()");
return 0;
}
let ffi_contact = unsafe { &*contact };
ffi_contact.contact.is_stale() as libc::c_int
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
@@ -4075,6 +4075,28 @@ pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> li
ffi_contact.contact.is_blocked() as libc::c_int
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_is_verified()");
return 0;
}
let ffi_contact = unsafe { &*contact };
let ctx = unsafe { &*ffi_contact.context };
if block_on(ffi_contact.contact.is_verified(ctx))
.context("is_verified failed")
.log_err(ctx)
.unwrap_or_default()
{
// Return value is essentially a boolean,
// but we return 2 for true for backwards compatibility.
2
} else {
0
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
@@ -4093,6 +4115,23 @@ pub unsafe extern "C" fn dc_contact_is_key_contact(contact: *mut dc_contact_t) -
unsafe { (*contact).contact.is_key_contact() as libc::c_int }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_verifier_id()");
return 0;
}
let ffi_contact = unsafe { &*contact };
let ctx = unsafe { &*ffi_contact.context };
let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(ctx))
.context("failed to get verifier")
.log_err(ctx)
.unwrap_or_default()
.unwrap_or_default()
.unwrap_or_default();
verifier_contact_id.to_u32()
}
// dc_lot_t
pub type dc_lot_t = lot::Lot;
@@ -4374,7 +4413,7 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
let ids = unsafe { std::slice::from_raw_parts(msg_ids, msg_cnt as usize) };
let msg_ids: Vec<MsgId> = ids
.iter()
.filter(|id| **id > MsgId::LAST_SPECIAL.to_u32())
.filter(|id| **id > DC_MSG_ID_LAST_SPECIAL)
.map(|id| MsgId::new(*id))
.collect();
@@ -4739,17 +4778,12 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
accounts: *const dc_accounts_t,
timeout_in_seconds: u64,
) -> libc::c_int {
if accounts.is_null() {
if accounts.is_null() || timeout_in_seconds <= 2 {
eprintln!("ignoring careless call to dc_accounts_background_fetch()");
return 0;
}
let accounts = unsafe { &*accounts };
if timeout_in_seconds <= 2 {
eprintln!("ignoring careless call to dc_accounts_background_fetch(): timeout too small");
block_on(accounts.read()).emit_event(EventType::AccountsBackgroundFetchDone);
return 0;
}
let background_fetch_future = {
let lock = block_on(accounts.read());
lock.background_fetch(Duration::from_secs(timeout_in_seconds))

View File

@@ -1,16 +0,0 @@
[package]
name = "deltachat-jsonrpc-bindings"
version = "2.61.0-dev"
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
edition = "2024"
license = "MPL-2.0"
repository = "https://github.com/chatmail/core"
[build-dependencies]
deltachat-jsonrpc = { workspace = true }
[dependencies]
[features]
default = ["vendored"]
vendored = ["deltachat-jsonrpc/vendored"]

View File

@@ -1,7 +0,0 @@
use deltachat_jsonrpc::api::{write_qt_bindings, write_ts_bindings};
use std::path::Path;
fn main() {
write_ts_bindings(Path::new("typescript/generated"));
write_qt_bindings(Path::new("qt/generated"), "deltachat");
}

View File

@@ -1 +0,0 @@
generated

View File

@@ -1,111 +0,0 @@
#pragma once
#include "deltachat.h"
#include "generated/client.hpp"
#include "generated/types.hpp"
#include <cstdint>
#include <mutex>
#include <thread>
namespace deltachat {
class CffiTransport : public Transport {
using CompletionHandler = Transport::CompletionHandler;
public:
explicit CffiTransport(dc_accounts_t *accounts)
: jsonrpc_(dc_jsonrpc_init(accounts)) {
if (!jsonrpc_)
std::abort();
thread_ = std::thread([this] { run(); });
}
~CffiTransport() override {
done_ = true;
// Unblock dc_jsonrpc_next_response by sending a dummy request
if (jsonrpc_)
dc_jsonrpc_request(
jsonrpc_,
"{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"get_system_info\"}");
if (thread_.joinable())
thread_.join();
std::lock_guard lk(mu_);
for (auto &[id, cb] : pending_) {
cb(Result<QJsonValue>::error(-32060, "Transport destructed"));
}
pending_.clear();
if (jsonrpc_)
dc_jsonrpc_unref(jsonrpc_);
}
virtual void send(const QString method, const QJsonValue params,
CompletionHandler onCompleted) override {
uint32_t id = next_id_++;
QJsonObject envelope{
{"jsonrpc", "2.0"},
{"id", static_cast<qint64>(id)},
{"method", method},
{"params", params},
};
{
std::lock_guard lk(mu_);
pending_[id] = std::move(onCompleted);
}
QByteArray json = QJsonDocument(envelope).toJson(QJsonDocument::Compact);
dc_jsonrpc_request(jsonrpc_, json.constData());
}
private:
void run() {
while (!done_) {
char *raw_json = dc_jsonrpc_next_response(jsonrpc_);
if (!raw_json) {
break;
}
QByteArray json{raw_json};
dc_str_unref(raw_json);
if (done_)
break;
QJsonObject obj = QJsonDocument::fromJson(json).object();
if (!obj["id"].isDouble()) {
qCritical() << "No valid rpc id in" << QString{json};
continue;
}
uint32_t id = static_cast<uint32_t>(obj["id"].toInt());
CompletionHandler cb;
{
std::lock_guard<std::mutex> lk(mu_);
if (auto nh = pending_.extract(id)) {
cb = std::move(nh.mapped());
} else {
qCritical() << "Could not map response" << QString{json};
continue;
}
}
cb(parseResult(obj));
}
}
private:
dc_jsonrpc_instance_t *jsonrpc_;
std::thread thread_;
std::mutex mu_;
std::atomic<uint32_t> next_id_{1};
std::atomic<bool> done_{false};
std::unordered_map<uint32_t, CompletionHandler> pending_;
};
class CffiDeltaChat : public RawClient {
public:
explicit CffiDeltaChat(dc_accounts_t *accounts)
: RawClient(std::make_unique<CffiTransport>(accounts)) {}
};
} // namespace deltachat
Q_DECLARE_METATYPE(deltachat::CffiDeltaChat *)

View File

@@ -1 +0,0 @@

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.61.0-dev"
version = "2.58.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2024"
license = "MPL-2.0"

View File

@@ -12,18 +12,19 @@ use deltachat::blob::BlobObject;
use deltachat::calls::ice_servers;
use deltachat::chat::{
self, Chat, ChatId, ChatItem, MessageListOptions, add_contact_to_chat, forward_msgs,
forward_msgs_2ctx, get_chat_media, get_chat_msgs, get_chat_msgs_ext, markfresh_chat,
forward_msgs_2ctx, get_chat_media, get_chat_msgs, get_chat_msgs_ex, markfresh_chat,
marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::{Config, get_all_ui_config_keys};
use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{Contact, ContactId, Origin, may_be_valid_addr};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::{
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ext, get_existing_msg_ids,
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ex, get_existing_msg_ids,
get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs,
};
use deltachat::peer_channels::{
@@ -63,8 +64,8 @@ use self::types::{
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
},
};
use crate::api::types::appversions::JsonrpcAppSource;
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
use crate::api::types::login_param::TransportListEntry;
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
#[derive(Debug)]
@@ -153,7 +154,7 @@ impl CommandApi {
}
}
#[rpc(all_positional)]
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
@@ -278,26 +279,9 @@ impl CommandApi {
/// Performs a background fetch for all accounts in parallel with a timeout.
///
/// For an account with IO stopped, the scheduler is paused
/// and every transport is fetched concurrently on a dedicated connection.
/// The account is done as soon as one transport received messages, the others stop.
/// Only one batch of messages is fetched per transport this way,
/// so a larger backlog is left to the next call or to started IO.
///
/// For an account with IO running, IMAP IDLE is interrupted on every transport
/// and the account is done once every transport is.
///
/// The call never waits for outgoing messages and never triggers sending them itself.
/// Received messages may still queue replies, securejoin handshakes for example,
/// which go out only while IO is running.
/// Use `is_sending_finished()` to tell whether the outgoing queue is empty.
///
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout,
/// and immediately if another background fetch is already running.
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create a generic notification if no message was fetched.
/// The event carries no data identifying the call it belongs to,
/// so it marks your own call only if no concurrent background fetch is happening.
/// without forgetting to create notifications caused by timing race conditions.
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
let future = {
let lock = self.accounts.read().await;
@@ -308,11 +292,6 @@ impl CommandApi {
Ok(())
}
/// Stops an ongoing `background_fetch()` call, making it return early
/// without waiting for the remaining transports or for the timeout.
///
/// The `AccountsBackgroundFetchDone` event is emitted as usual.
/// Does nothing if no background fetch is running.
async fn stop_background_fetch(&self) -> Result<()> {
self.accounts.read().await.stop_background_fetch();
Ok(())
@@ -523,7 +502,8 @@ impl CommandApi {
/// - [Self::add_transport_from_qr()] to add a transport
/// from a server encoded in a QR code.
/// - [Self::list_transports()] to get a list of all configured transports.
/// - [Self::delete_transport()] to remove a transport.
/// - [Self::set_transport_unpublished()] to remove a transport.
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
async fn add_or_update_transport(
&self,
account_id: u32,
@@ -548,8 +528,32 @@ impl CommandApi {
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_or_update_transport()] to add or change a transport
/// and [Self::delete_transport()] to remove a transport.
/// and [Self::set_transport_unpublished()] to remove a transport.
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
let ctx = self.get_context(account_id).await?;
let res = ctx
.list_transports()
.await?
.into_iter()
.filter(|t| !t.is_unpublished)
.map(|t| t.param.into())
.collect();
Ok(res)
}
/// Deprecated 2026-06: This is not needed by UI implementations anymore,
/// because unpublished relays now count as removed from the user point of view,
/// and must not be shown in the list of relays.
/// This means that UIs should use `list_transports()` instead of this function.
///
/// Returns the list of all email accounts that are used as a transport in the current profile.
///
/// As opposed to `list_transports()`, this function also returns unpublished transports,
/// and for each returned transport it returns the information whether or not is `unpublished`.
///
/// Use [Self::add_or_update_transport()] to add or change a transport
/// and [Self::set_transport_unpublished()] to change whether a transport is 'published'.
async fn list_transports_ex(&self, account_id: u32) -> Result<Vec<TransportListEntry>> {
let ctx = self.get_context(account_id).await?;
let res = ctx
.list_transports()
@@ -560,17 +564,41 @@ impl CommandApi {
Ok(res)
}
/// Removes a transport.
/// UIs should call this function when the user removes a relay.
///
/// The last transport cannot be removed.
/// If the removed transport was the one used for sending,
/// another one is chosen automatically.
/// Immediately deletes a transport, potentially causing messages not to arrive.
/// This must ONLY be used by the automated tests.
/// UI implementations must use [`Self::set_transport_unpublished`] instead.
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.delete_transport(&addr).await
}
/// Change whether the transport is unpublished.
/// UIs should call this function when the user clicks on "Remove".
/// Core will keep listening on this transport for some time,
/// and automatically remove it once it is no longer needed.
///
/// Unpublished transports are not advertised to contacts,
/// and self-sent messages are not sent there,
/// so that we don't cause extra messages to the corresponding inbox,
/// but can still receive messages from contacts who don't know our new transport addresses yet.
///
/// When more transports are added by [`Self::add_or_update_transport()`] or [`Self::add_transport_from_qr`],
/// the least recently needed unpublished transport is automatically removed
/// if this is necessary in order to stay below the maximum number of allowed relays.
/// Also, unpublished transports that are not used to receive any new messages for a time defined by
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`] are automatically removed.
///
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`]: deltachat::sql::UNPUBLISHED_TRANSPORT_KEEP_TIME
async fn set_transport_unpublished(
&self,
account_id: u32,
addr: String,
unpublished: bool,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.set_transport_unpublished(&addr, unpublished).await
}
/// Signal an ongoing process to stop.
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -847,8 +875,6 @@ impl CommandApi {
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
///
/// To reset invitations, pass the link to `set_config_from_qr()`.
///
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
/// If `chat_id` is unset, setup contact QR code is returned.
async fn get_chat_securejoin_qr_code(
@@ -862,19 +888,20 @@ impl CommandApi {
Ok(qr)
}
/// Get QR code (text and SVG) that will offer a SecureJoin invitation.
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
/// The QR code is compatible to the OPENPGP4FPR format
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
///
/// The scanning device will pass the scanned content to `checkQr()` then;
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
/// the securejoin protocol can be started using `secure_join()`
/// an out-of-band-verification can be joined using `secure_join()`
///
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
///
/// chat_id: If set to a group-chat-id,
/// the SecureJoin QR code for the group is returned.
/// If not set, the setup contact QR code is returned.
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
/// If not set, the Setup-Contact protocol is offered in the QR code.
/// See https://securejoin.delta.chat/ for details about both protocols.
///
/// return format: `[code, svg]`
@@ -890,7 +917,7 @@ impl CommandApi {
Ok((qr, svg))
}
/// Continue the SecureJoin protocol
/// Continue a Setup-Contact or Verified-Group-Invite protocol
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
/// This function is typically called when `check_qr()` returns
/// type=AskVerifyContact or type=AskVerifyGroup.
@@ -908,6 +935,7 @@ impl CommandApi {
/// to `check_qr()`.
///
/// **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
/// A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
///
async fn secure_join(&self, account_id: u32, qr: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
@@ -973,6 +1001,8 @@ impl CommandApi {
/// If the group is already _promoted_ (any message was sent to the group),
/// all group members are informed by a special status message that is sent automatically by this function.
///
/// If the group has group protection enabled, only verified contacts can be added to the group.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
async fn add_contact_to_chat(
&self,
@@ -1352,7 +1382,7 @@ impl CommandApi {
///
/// * chat_id The chat ID of which the messages IDs should be queried.
/// * _info_only: Deprecated, pass `false` here.
/// * `add_daymarker` - If `true`, add day markers as `MsgId::DAYMARKER` to the result,
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
/// corresponding (following) day in the local timezone.
async fn get_message_ids(
@@ -1363,7 +1393,7 @@ impl CommandApi {
add_daymarker: bool,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ext(
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions { add_daymarker },
@@ -1374,7 +1404,7 @@ impl CommandApi {
.map(|chat_item| -> u32 {
match chat_item {
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
deltachat::chat::ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
}
})
.collect())
@@ -1412,7 +1442,7 @@ impl CommandApi {
add_daymarker: bool,
) -> Result<Vec<JsonrpcMessageListItem>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ext(
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions { add_daymarker },
@@ -1478,32 +1508,12 @@ impl CommandApi {
MessageNotificationInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
}
/// Sets the "pinned" state for a message.
async fn set_pinned_message_state(
&self,
account_id: u32,
message_id: u32,
pinned_state: bool,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
deltachat::pinned_messages::set_pinned_state(&ctx, MsgId::new(message_id), pinned_state)
.await
}
/// Returns all pinned messages of a chat.
async fn get_pinned_messages(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_ids =
deltachat::pinned_messages::get_pinned_messages(&ctx, ChatId::new(chat_id)).await?;
Ok(msg_ids.into_iter().map(|id| id.to_u32()).collect())
}
/// Delete messages. The messages are deleted on the current device and
/// on the IMAP server.
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
delete_msgs_ext(&ctx, &msgs, false).await
delete_msgs_ex(&ctx, &msgs, false).await
}
/// Delete messages. The messages are deleted on the current device,
@@ -1511,7 +1521,7 @@ impl CommandApi {
async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
delete_msgs_ext(&ctx, &msgs, true).await
delete_msgs_ex(&ctx, &msgs, true).await
}
/// Get an informational text for a single message. The text is multiline and may
@@ -1794,7 +1804,7 @@ impl CommandApi {
/// Get encryption info for a contact.
/// Get a multi-line encryption info, containing your fingerprint and the
/// fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
/// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
async fn get_contact_encryption_info(
&self,
account_id: u32,
@@ -2059,20 +2069,15 @@ impl CommandApi {
Ok(())
}
/// Waits until all transports are idle or failed and no background work is left.
/// Never returns unless I/O is started. Must ONLY be used by tests.
async fn wait_for_all_work_done(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.wait_for_all_work_done().await;
Ok(())
}
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
/// One of:
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000): Show e.g. the string "Getting new messages" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (4000): Show e.g. the string "Connected" or a green dot
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
///
/// We don't use exact values but ranges here so that we can split up
/// states into multiple states in the future.
///
/// Meant as a rough overview that can be shown
/// e.g. in the title of the main screen.
@@ -2256,9 +2261,6 @@ impl CommandApi {
/// Get blob encoded as base64 from a webxdc message
///
/// path is the path of the file within webxdc archive
///
/// If the file is `icon.png` or `icon.jpg`,
/// loading it may fail if dimensions are unexpectedly large.
async fn get_webxdc_blob(
&self,
account_id: u32,
@@ -2439,7 +2441,6 @@ impl CommandApi {
}
/// Returns reactions to the message.
/// `None` when there are no reactions.
async fn get_message_reactions(
&self,
account_id: u32,
@@ -2771,48 +2772,6 @@ impl CommandApi {
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
}
}
/// Get version information of a specific client and source
/// across all configured accounts and transports.
///
/// Returns the source with the highest `version_integer`.
/// If no matching version information is available at all, `None` is returned.
///
/// UIs shall call the function after a reasonable time after app start,
/// when most relays have reported the information they have, say 30 seconds.
/// After that, once a day.
/// (it is accepted if by the simple approach an update message is delayed.
/// an event was considered, but that seemed more complex for few benefit:
/// as we do not know if "late" relays will report "better" versions,
/// also there we would work with timeouts etc.)
///
/// If the reported `version_integer` is larger than the running app version,
/// the UI shall report to the user, that an update is available,
/// and, if possible, offer a direct update by the given URL.
///
/// Security note: consumers need to verify themselves
/// that downloaded app files are valid before installing them.
async fn get_app_version(
&self,
client_id: String,
source_id: String,
) -> Result<Option<JsonrpcAppSource>> {
let accounts = self.accounts.read().await;
Ok(
deltachat::appversions::get_app_version(&accounts, &client_id, &source_id)
.await?
.map(JsonrpcAppSource::from_core_type),
)
}
/// Returns true if all accounts have empty outgoing message queue.
///
/// This API is intended to be used by UIs
/// to request that operating system does not put the application in background
/// while there are still outgoing messages that are not sent out.
async fn is_sending_finished(&self) -> Result<bool> {
self.accounts.read().await.is_sending_finished().await
}
}
// Helper functions (to prevent code duplication)

View File

@@ -1,31 +0,0 @@
use deltachat::appversions::AppSource;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
/// Version information of a single source of a client, eg. "gplay" or "fdroid".
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "AppSource", rename_all = "camelCase")]
pub struct JsonrpcAppSource {
/// Always increasing version number.
pub version_integer: u32,
/// Version string that should be shown to the user.
/// UI must not linkify the string
/// as it may be interpreted like a phone number or an IP address.
pub version_string: String,
/// Where to download that version.
/// Security note: consumers need to verify themselves
/// that downloaded app files are valid before installing them.
pub download_url: String,
}
impl JsonrpcAppSource {
pub fn from_core_type(source: AppSource) -> Self {
JsonrpcAppSource {
version_integer: source.version_integer,
version_string: source.version_string,
download_url: source.download_url,
}
}
}

View File

@@ -31,6 +31,37 @@ pub struct ContactObject {
/// e.g. if we just scanned the fingerprint from a QR code.
e2ee_avail: bool,
/// True if the contact
/// can be added to protected chats
/// because SELF and contact have verified their fingerprints in both directions.
///
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
is_verified: bool,
/// The contact ID that verified a contact.
///
/// As verifier may be unknown,
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
///
/// UI should display the information in the contact's profile as follows:
///
/// - If `verifierId` != 0,
/// display text "Introduced by ..."
/// with the name of the contact.
/// Prefix the text by a green checkmark.
///
/// - If `verifierId` == 0 and `isVerified` != 0,
/// display "Introduced" prefixed by a green checkmark.
///
/// - if `verifierId` == 0 and `isVerified` == 0,
/// display nothing
///
/// This contains the contact ID of the verifier.
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
/// If it is None/Null, we don't have verifier information or
/// the contact is not verified.
verifier_id: Option<u32>,
/// the contact's last seen timestamp
last_seen: i64,
was_seen_recently: bool,
@@ -48,6 +79,14 @@ impl ContactObject {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let is_verified = contact.is_verified(context).await?;
let verifier_id = contact
.get_verifier_id(context)
.await?
.flatten()
.map(|contact_id| contact_id.to_u32());
Ok(ContactObject {
address: contact.get_addr().to_owned(),
color: color_int_to_hex_string(contact.get_color()),
@@ -61,6 +100,8 @@ impl ContactObject {
is_blocked: contact.is_blocked(),
is_key_contact: contact.is_key_contact(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
is_bot: contact.is_bot(),

View File

@@ -337,7 +337,8 @@ pub enum EventType {
contact_id: u32,
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 1000=vg-member-added/vc-contact-confirm received
progress: u16,
},
@@ -393,15 +394,11 @@ pub enum EventType {
msg_id: u32,
},
/// Tells that a background fetch call is done:
/// the fetch completed, timed out, was stopped or was not started.
/// Tells that the Background fetch was completed (or timed out).
/// This event acts as a marker, when you reach this event you can be sure
/// that all events emitted during the background fetch were processed.
///
/// For the call that started the fetch, this event acts as a marker:
/// all events emitted during the fetch were processed once it is reached.
/// A call made while another background fetch is running gets the event immediately,
/// and the running fetch keeps emitting events until its own marker.
///
/// This event is only emitted by the account manager.
/// This event is only emitted by the account manager
AccountsBackgroundFetchDone,
/// Inform that set of chats or the order of the chats in the chatlist has changed.
///
@@ -481,9 +478,9 @@ pub enum EventType {
///
/// UI should update the list.
///
/// The event is emitted on the device modifying
/// the transports as well as on other devices
/// applying the synced change.
/// This event is emitted when transport
/// synchronization messages arrives,
/// but not when the UI modifies the transport list by itself.
TransportsModified,
}

View File

@@ -4,6 +4,16 @@ use serde::Deserialize;
use serde::Serialize;
use yerpc::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TransportListEntry {
/// The login data entered by the user.
pub param: EnteredLoginParam,
/// Whether this transport is set to 'unpublished'.
/// See `set_transport_unpublished` / `setTransportUnpublished` for details.
pub is_unpublished: bool,
}
/// Login parameters entered by the user.
///
/// Usually it will be enough to only set `addr` and `password`,
@@ -58,6 +68,15 @@ pub struct EnteredLoginParam {
pub certificate_checks: Option<EnteredCertificateChecks>,
}
impl From<dc::TransportListEntry> for TransportListEntry {
fn from(transport: dc::TransportListEntry) -> Self {
TransportListEntry {
param: transport.param.into(),
is_unpublished: transport.is_unpublished,
}
}
}
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
fn from(param: dc::EnteredLoginParam) -> Self {
let imap_security: Socket = param.imap.security.into();

View File

@@ -103,9 +103,6 @@ pub struct MessageObject {
saved_message_id: Option<u32>,
is_pinned: bool,
/// `None` when there are no reactions.
reactions: Option<JsonrpcReactions>,
vcard_contact: Option<VcardContact>,
@@ -266,7 +263,6 @@ impl MessageObject {
.await?
.map(|id| id.to_u32()),
is_pinned: message.is_pinned(),
reactions,
vcard_contact: vcard_contacts.first().cloned(),
@@ -429,8 +425,6 @@ pub enum SystemMessageType {
CallAccepted,
CallEnded,
MessagePinned,
MessageUnpinned,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
@@ -460,8 +454,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
SystemMessage::CallEnded => SystemMessageType::CallEnded,
SystemMessage::MessagePinned => SystemMessageType::MessagePinned,
SystemMessage::MessageUnpinned => SystemMessageType::MessageUnpinned,
}
}
}
@@ -734,9 +726,9 @@ impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
fn from(value: deltachat::ephemeral::Timer) -> Self {
match value {
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
deltachat::ephemeral::Timer::Enabled { duration } => EphemeralTimer::Enabled {
duration: duration.get(),
},
deltachat::ephemeral::Timer::Enabled { duration } => {
EphemeralTimer::Enabled { duration }
}
}
}
}

View File

@@ -1,5 +1,4 @@
pub mod account;
pub mod appversions;
pub mod calls;
pub mod chat;
pub mod chat_list;

View File

@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "kind")]
pub enum QrObject {
/// Ask the user whether to start chatting with the contact.
/// Ask the user whether to verify the contact.
///
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
AskVerifyContact {
@@ -61,7 +61,7 @@ pub enum QrObject {
/// Whether the inviter supports the new Securejoin v3 protocol
is_v3: bool,
},
/// Contact fingerprint matches.
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
FprOk {

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use deltachat::contact::ContactId;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -23,11 +24,8 @@ pub struct JsonrpcReaction {
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JsonrpcReactions {
/// Map from a contact to it's reaction to message.
///
/// There is only a single reaction per contact,
/// but this contains a list of reactions for historical reasons.
///
/// For channels subscribers, this map is empty or contains `ContactId::SELF` only.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count, sorted in descending order.
reactions: Vec<JsonrpcReaction>,
@@ -36,24 +34,30 @@ pub struct JsonrpcReactions {
impl From<Reactions> for JsonrpcReactions {
fn from(reactions: Reactions) -> Self {
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
.by_contact
.iter()
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
.collect();
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());
let reactions = reactions
.frequencies
.into_iter()
.map(|entry| JsonrpcReaction {
emoji: entry.reaction.as_str().to_string(),
count: entry.count,
is_from_self: entry.is_from_self,
})
.collect();
let mut reactions_v = Vec::new();
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
let is_from_self = if let Some(self_reaction) = self_reaction {
self_reaction.contains(&emoji)
} else {
false
};
let reaction = JsonrpcReaction {
emoji,
count,
is_from_self,
};
reactions_v.push(reaction)
}
JsonrpcReactions {
reactions_by_contact,
reactions,
reactions: reactions_v,
}
}
}

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.61.0-dev"
"version": "2.58.0-dev"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.61.0-dev"
version = "2.58.0-dev"
license = "MPL-2.0"
edition = "2024"
repository = "https://github.com/chatmail/core"

View File

@@ -228,7 +228,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id == MsgId::DAYMARKER {
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
println!(
"--------------------------------------------------------------------------------"
);
@@ -259,13 +259,19 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
let addr = contact.get_addr();
let verified_str = if contact.is_verified(context).await? {
""
} else {
""
};
let line = format!(
"{} <{}>",
"{}{} <{}>",
if !name.is_empty() {
name
} else {
"<name unset>"
},
verified_str,
if !addr.is_empty() { addr } else { "addr unset" }
);
@@ -610,7 +616,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let sel_chat = sel_chat.as_ref().unwrap();
let time_start = std::time::SystemTime::now();
let msglist = chat::get_chat_msgs_ext(
let msglist = chat::get_chat_msgs_ex(
&context,
sel_chat.get_id(),
chat::MessageListOptions {
@@ -624,7 +630,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.into_iter()
.map(|x| match x {
ChatItem::Message { msg_id } => msg_id,
ChatItem::DayMarker { .. } => MsgId::DAYMARKER,
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
})
.collect();

View File

@@ -173,7 +173,7 @@ const DB_COMMANDS: [&str; 10] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 38] = [
const CHAT_COMMANDS: [&str; 39] = [
"listchats",
"listarchived",
"start-realtime",
@@ -182,6 +182,7 @@ const CHAT_COMMANDS: [&str; 38] = [
"createchat",
"creategroup",
"createbroadcast",
"createprotected",
"addmember",
"removemember",
"groupname",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.61.0-dev"
version = "2.58.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [

View File

@@ -157,7 +157,7 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
"""
# You removed member a@b.
# You added member a@b.
# You were removed by a@b.
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
@@ -36,15 +35,6 @@ class Account:
if event_type is None or next_event.kind == event_type:
return next_event
def wait_for_realtime_data(self, msg_id: int) -> bytes:
"""Wait for the next realtime data received for the given webxdc message and return it."""
logging.info(f"account {self.id}: waiting for realtime data for msg {msg_id}")
while True:
event = self.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
if event.msg_id == msg_id:
logging.info(f"account {self.id}: got realtime data for msg {msg_id}: {event.data[:20]}")
return bytes(event.data)
def clear_all_events(self):
"""Remove all queued-up events for a given account.
@@ -143,6 +133,10 @@ class Account:
"""Delete a transport."""
self._rpc.delete_transport(self.id, addr)
def set_transport_unpublished(self, addr: str, unpublished: bool = True):
"""Unpublish the transport."""
self._rpc.set_transport_unpublished(self.id, addr, unpublished)
@futuremethod
def list_transports(self):
"""Return the list of all email accounts that are used as a transport in the current profile."""
@@ -150,10 +144,9 @@ class Account:
return transports
def bring_online(self):
"""Start I/O, wait until all transports became IDLE and drop the events seen so far."""
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
self._rpc.wait_for_all_work_done(self.id)
self.clear_all_events()
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
@@ -272,7 +265,7 @@ class Account:
return Contact(self, SpecialContactId.SELF)
@property
def device_contact(self) -> Contact:
def device_contact(self) -> Chat:
"""Account's device contact."""
return Contact(self, SpecialContactId.DEVICE)
@@ -360,7 +353,7 @@ class Account:
return Chat(self, chat_id)
def secure_join(self, qrdata: str) -> Chat:
"""Continue the SecureJoin protocol started on another device.
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
The function returns immediately and the handshake runs in background, sending
and receiving several messages.

View File

@@ -70,7 +70,6 @@ class EventType(str, Enum):
SELFAVATAR_CHANGED = "SelfavatarChanged"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
ACCOUNTS_BACKGROUND_FETCH_DONE = "AccountsBackgroundFetchDone"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
ACCOUNTS_CHANGED = "AccountsChanged"

View File

@@ -48,13 +48,6 @@ class DeltaChat:
"""Stop ongoing background fetch."""
self.rpc.stop_background_fetch()
def wait_for_event(self, event_type=None) -> AttrDict:
"""Wait until the next account manager event and return it."""
while True:
next_event = AttrDict(self.rpc.wait_for_event(0))
if event_type is None or next_event.kind == event_type:
return next_event
def maybe_network(self) -> None:
"""Indicate that the network conditions might have changed."""
self.rpc.maybe_network()
@@ -74,7 +67,3 @@ class DeltaChat:
def stop_sending_locations(self) -> None:
"""Stop sending locations to all chats."""
return self.rpc.stop_sending_locations()
def is_sending_finished(self) -> bool:
"""Return true if sending queues of all accounts are empty."""
return self.rpc.is_sending_finished()

View File

@@ -121,7 +121,7 @@ class Message:
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
@futuremethod
def send_webxdc_realtime_data(self, data):
def send_webxdc_realtime_data(self, data) -> None:
"""Send data to the realtime channel."""
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))

View File

@@ -7,12 +7,10 @@ import os
import pathlib
import platform
import random
import socket
import subprocess
import sys
import time
import urllib.parse
from typing import Iterator, Optional
from typing import AsyncGenerator, Optional
import pytest
@@ -27,42 +25,19 @@ Currently this is "Messages are end-to-end encrypted."
"""
def pytest_configure(config):
# Run only in the xdist controller, before the workers exist.
if not hasattr(config, "workerinput"):
domain = os.environ.get("CHATMAIL_DOMAIN")
if domain:
check_chatmail_domain_and_warmup_dns_cache(domain)
def check_chatmail_domain_and_warmup_dns_cache(domain):
for i in range(6):
try:
socket.getaddrinfo(domain, 443)
return
except socket.gaierror as e:
error = e
logging.warning(f"DNS resolution of {domain} failed (attempt {i}): {e}")
time.sleep(10)
pytest.exit(f"cannot resolve chatmail relay domain {domain}: {error}")
def pytest_report_header():
headers = [f"CHATMAIL_DOMAIN: {os.environ.get('CHATMAIL_DOMAIN')}"]
for base in os.get_exec_path():
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
if fn.exists():
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
proc.wait()
version = proc.stderr.read().decode().strip()
headers.append(f"RPC-SERVER: {fn} [{version}]")
break
return f"deltachat-rpc-server: {fn} [{version}]"
return headers
return None
class RPCAccountFactory:
class ACFactory:
"""Test account factory."""
def __init__(self, deltachat: DeltaChat) -> None:
@@ -76,7 +51,7 @@ class RPCAccountFactory:
"""Create a new unconfigured bot."""
return Bot(self.get_unconfigured_account())
def get_credentials(self) -> tuple[str, str]:
def get_credentials(self) -> (str, str):
"""Generate new credentials for chatmail account."""
domain = os.environ["CHATMAIL_DOMAIN"]
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
@@ -175,7 +150,7 @@ class RPCAccountFactory:
@pytest.fixture
def rpc(tmp_path) -> Iterator[Rpc]:
def rpc(tmp_path) -> AsyncGenerator:
"""RPC client fixture."""
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
@@ -189,13 +164,13 @@ def dc(rpc) -> DeltaChat:
@pytest.fixture
def acf(dc) -> RPCAccountFactory:
def acfactory(dc) -> AsyncGenerator:
"""Return account factory fixture."""
return RPCAccountFactory(dc)
return ACFactory(dc)
@pytest.fixture
def rpcdata():
def data():
"""Test data."""
class Data:
@@ -292,7 +267,7 @@ def get_core_python_env(tmp_path_factory):
@pytest.fixture
def alice_and_remote_bob(tmp_path, acf, get_core_python_env):
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
The 'eval' function allows to remote-execute arbitrary expressions
@@ -306,19 +281,20 @@ def alice_and_remote_bob(tmp_path, acf, get_core_python_env):
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
channel = gw.remote_exec(remote_bob_loop)
cm = os.environ.get("CHATMAIL_DOMAIN")
# old cores need "ic=3" to accept
# the self-signed cert of an underscore domain
addr, password = acf.get_credentials()
addr, password = acfactory.get_credentials()
dclogin_qr = f"dclogin://{urllib.parse.quote(addr, safe='@')}?p={urllib.parse.quote(password)}&v=1"
if os.environ["CHATMAIL_DOMAIN"].startswith("_"):
if cm and cm.startswith("_"):
dclogin_qr += "&ic=3"
# trigger getting an online account on bob's side
channel.send((accounts_dir, str(rpc_server_path), dclogin_qr))
# meanwhile get a local alice account
alice = acf.get_online_account()
alice = acfactory.get_online_account()
channel.send(alice.self_contact.make_vcard())
# wait for bob to have started
@@ -360,10 +336,10 @@ def remote_bob_loop(channel):
dc = DeltaChat(rpc)
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
# RPCAccountFactory would configure from a "dcaccount" QR,
# ACFactory would configure from a "dcaccount" QR,
# which old cores cannot use on underscore domains
bob = dc.add_account()
bob.add_transport_from_qr(dclogin_qr)
bob.set_config_from_qr(dclogin_qr)
bob.bring_online()
alice_vcard = channel.receive()

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import contextlib
import itertools
import json
import logging
@@ -39,15 +38,8 @@ class RpcMethod:
"params": args,
"id": request_id,
}
queue: Queue = Queue()
# Register before testing for shutdown, so that either the reader loop
# finds this request while draining, or the test below catches it here.
# Testing first would race with the reader loop finishing in between.
self.rpc.request_results[request_id] = queue
if self.rpc.request_queue_closed:
self.rpc._fail_request(request_id)
else:
self.rpc.request_queue.put(request)
self.rpc.request_results[request_id] = queue = Queue()
self.rpc.request_queue.put(request)
def rpc_future():
"""Wait for the request to receive a result."""
@@ -86,10 +78,6 @@ class Rpc:
# Map from request ID to a Queue which provides a single result
self.request_results: dict[int, Queue]
self.request_queue: Queue[Any]
# Emulates `request_queue.shutdown(immediate=False)`, which needs Python 3.13:
# https://github.com/python/cpython/blob/v3.13.0/Lib/queue.py#L236-L257
# Note that `request_queue_closed` is set by the reader loop.
self.request_queue_closed: bool
self.closing: bool
self.reader_thread: Thread
self.writer_thread: Thread
@@ -119,7 +107,6 @@ class Rpc:
self.event_queues = {}
self.request_results = {}
self.request_queue = Queue()
self.request_queue_closed = False
self.closing = False
self.reader_thread = Thread(target=self.reader_loop)
self.reader_thread.start()
@@ -136,8 +123,6 @@ class Rpc:
# The reader_loop already saw EOF on stdout, so the process
# has exited and stderr is available.
stderr = self.process.stderr.read().decode(errors="replace").strip()
self.closing = True
self._shutdown_loops()
if stderr:
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
@@ -150,31 +135,11 @@ class Rpc:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
self.stop_io_for_all_accounts()
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
# otherwise it might exit through an "RPC server closed" error instead.
self.events_thread.join()
self._shutdown_loops()
def _shutdown_loops(self) -> None:
"""Close the server pipe and wait for the loop threads to finish.
The writer blocks on an empty request queue,
so it needs the sentinel to notice the shutdown.
"""
with contextlib.suppress(BrokenPipeError):
# An exited server may leave data unflushed,
# which close() would try to write out again.
self.process.stdin.close()
self.request_queue.put(None)
self.process.stdin.close()
self.reader_thread.join()
self.request_queue.put(None)
self.writer_thread.join()
self.events_thread.join()
def _fail_request(self, request_id: int) -> None:
"""Answer a registered request with an error, unless it was answered already."""
queue = self.request_results.pop(request_id, None)
if queue is not None:
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
def __enter__(self):
self.start()
@@ -197,11 +162,9 @@ class Rpc:
# Log an exception if the reader loop dies.
logging.exception("Exception in the reader loop")
finally:
# Shut the request queue first, so that requests registered from now
# on are failed by their caller, then answer the pending ones here.
self.request_queue_closed = True
for request_id in list(self.request_results):
self._fail_request(request_id)
# Unblock any pending requests when the server closes stdout.
for _request_id, queue in self.request_results.items():
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
def writer_loop(self) -> None:
"""Writer loop ensuring only a single thread writes requests."""

View File

@@ -22,10 +22,8 @@ ALL = "1:*"
class DirectImap:
"""Internal Python-level IMAP handling."""
def __init__(self, account: Account, addr=None, password=None) -> None:
def __init__(self, account: Account) -> None:
self.account = account
self.addr = addr or account.get_config("addr")
self.password = password or account.get_config("mail_pw")
self.logid = account.get_config("displayname") or id(account)
self._idling = False
self.connect()
@@ -35,9 +33,9 @@ class DirectImap:
host = self.account.get_config("configured_mail_server")
port = 993
user = self.addr
user = self.account.get_config("addr")
host = user.rsplit("@")[-1]
pw = self.password
pw = self.account.get_config("mail_pw")
ssl_context = ssl.create_default_context()
if host.startswith("_"):
@@ -171,7 +169,7 @@ class DirectImap:
self.conn.append(bytes(msg, encoding="ascii"), folder)
def get_uid_by_message_id(self, message_id) -> str:
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)), mark_seen=False)]
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]
@@ -180,6 +178,9 @@ class DirectImap:
class IdleManager:
def __init__(self, direct_imap) -> None:
self.direct_imap = direct_imap
# fetch latest messages before starting idle so that it only
# returns messages that arrive anew
self.direct_imap.conn.fetch("1:*")
self.direct_imap.conn.idle.start()
def check(self, timeout=None) -> list[bytes]:

View File

@@ -5,16 +5,16 @@ from typing import TYPE_CHECKING
from deltachat_rpc_client import EventType
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
from deltachat_rpc_client.pytestplugin import ACFactory
def test_event_on_configuration(acf: RPCAccountFactory) -> None:
def test_event_on_configuration(acfactory: ACFactory) -> None:
"""
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
"""
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.clear_all_events()
assert not account.is_configured()
future = account.add_or_update_transport.future({"addr": addr, "password": password})

View File

@@ -1,8 +1,8 @@
from deltachat_rpc_client import EventType, Message
def test_calls(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_calls(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
place_call_info = "offer"
accept_call_info = "answer"
@@ -35,14 +35,14 @@ def test_calls(acf) -> None:
assert incoming_call_message.get_call_info().state.kind == "Completed"
def test_video_call(acf) -> None:
def test_video_call(acfactory) -> None:
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
# with `s= ` replaced with `s=-`.
#
# `s=` cannot be empty according to RFC 3264,
# so it is more clear as `s=-`.
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -57,8 +57,8 @@ def test_video_call(acf) -> None:
assert incoming_call_message.get_call_info().has_video
def test_audio_call(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_audio_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -73,15 +73,15 @@ def test_audio_call(acf) -> None:
assert not incoming_call_message.get_call_info().has_video
def test_ice_servers(acf) -> None:
alice = acf.get_online_account()
def test_ice_servers(acfactory) -> None:
alice = acfactory.get_online_account()
ice_servers = alice.ice_servers()
assert len(ice_servers) == 1
def test_no_contact_request_call(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_no_contact_request_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
@@ -101,8 +101,8 @@ def test_no_contact_request_call(acf) -> None:
break
def test_who_can_call_me_nobody(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_who_can_call_me_nobody(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (2)
bob.set_config("who_can_call_me", "2")
@@ -128,9 +128,9 @@ def test_who_can_call_me_nobody(acf) -> None:
break
def test_who_can_call_me_everybody(acf) -> None:
def test_who_can_call_me_everybody(acfactory) -> None:
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (0)
bob.set_config("who_can_call_me", "0")

View File

@@ -2,10 +2,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from deltachat_rpc_client import EventType, const
from deltachat_rpc_client import Account, EventType, const
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
from deltachat_rpc_client.pytestplugin import ACFactory
def wait_for_chatlist_and_specific_item(account, chat_id):
@@ -40,11 +40,11 @@ def wait_for_chatlist(account):
break
def test_delivery_status(acf: RPCAccountFactory) -> None:
def test_delivery_status(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes (delivered, read)
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -82,11 +82,11 @@ def test_delivery_status(acf: RPCAccountFactory) -> None:
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
def test_delivery_status_failed(acf: RPCAccountFactory) -> None:
def test_delivery_status_failed(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes failed
"""
(alice,) = acf.get_online_accounts(1)
(alice,) = acfactory.get_online_accounts(1)
alice.set_config("force_encryption", "0")
invalid_contact = alice.create_contact("example@example.com", "invalid address")
@@ -110,12 +110,12 @@ def test_delivery_status_failed(acf: RPCAccountFactory) -> None:
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
def test_download_on_demand(acfactory: ACFactory, data) -> None:
"""
Test if download on demand emits chatlist update events.
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -128,7 +128,7 @@ def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
"Hello World, this message is bigger than 5 bytes",
file=rpcdata.get_path("image/screenshot.jpg"),
file=data.get_path("image/screenshot.jpg"),
)
message = alice.wait_for_incoming_msg()
@@ -144,8 +144,8 @@ def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
wait_for_chatlist_specific_item(alice, chat_id)
def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
alice, bob = acf.get_online_accounts(2)
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -161,12 +161,12 @@ def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
return [alice, alice_second_device, bob, alice_chat_bob]
def test_imap_sync_seen_msgs(acf: RPCAccountFactory) -> None:
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
"""
Test that chatlist changed events are emitted for the second device
when the message is marked as read on the first device
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
bob.create_chat(alice)
@@ -191,11 +191,11 @@ def test_imap_sync_seen_msgs(acf: RPCAccountFactory) -> None:
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
def test_multidevice_sync_chat(acf: RPCAccountFactory) -> None:
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
"""
Test multidevice sync: syncing chat visibility and muting across multiple devices
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice_chat_bob.archive()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)

View File

@@ -1,5 +1,4 @@
import subprocess
import time
import pytest
@@ -17,7 +16,7 @@ def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
def test_qr_setup_contact(acfactory, alice_and_remote_bob, version) -> None:
"""Test other-core Bob profile can do securejoin with Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
@@ -25,48 +24,28 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.e2ee_avail
assert alice_contact_bob_snapshot.is_verified
remote_eval("bob.wait_for_securejoin_joiner_success()")
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
# Test that Bob verified Alice's profile.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
# Test that Bob can also scan a QR code
# of Alice for which the key is not known yet.
# For the test above Bob already knew the key from a vCard.
alice2 = acf.get_online_account()
alice2 = acfactory.get_online_account()
qr_code = alice2.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
remote_eval("bob.wait_for_securejoin_joiner_success()")
alice2.wait_for_securejoin_inviter_success()
@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact_multitransport(acf, alice_and_remote_bob, version) -> None:
"""Test other-core Bob profile can do securejoin with Alice on current core, with multiple transports."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
relay_qr = acf.get_account_qr()
alice.add_transport_from_qr(relay_qr)
alice.add_transport_from_qr(relay_qr)
qr_code = alice.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success()
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.e2ee_avail
remote_eval("bob.wait_for_securejoin_joiner_success()")
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
def test_send_and_receive_message(alice_and_remote_bob) -> None:
"""Test other-core Bob profile can send a message to Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.24.0")
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
@@ -74,149 +53,14 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
assert msg.get_snapshot().text == "hello"
def test_second_device(acf, alice_and_remote_bob) -> None:
def test_second_device(acfactory, alice_and_remote_bob) -> None:
"""Test setting up current version as a second device for old version."""
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.24.0")
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
new_account = acf.get_unconfigured_account()
new_account = acfactory.get_unconfigured_account()
new_account._rpc.get_backup(new_account.id, qr)
remote_eval("locals()['future']()")
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
@pytest.mark.parametrize("replace_relay", [False, True], ids=["add", "replace"])
def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, replace_relay):
"""Test 2.48 Bob learns a relay change of Alice from a keyupdate, and is shown nothing."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.48.0")
def bob_sees():
return remote_eval(
"{'chats': len(bob.get_chatlist()),"
" 'fresh': len(bob._rpc.get_fresh_msgs(bob.id)),"
" 'contacts': len(bob.get_contacts()),"
" 'alice_chat': bob._rpc.get_chat_id_by_contact_id(bob.id, bob_contact_alice.id) or 0}",
)
# Keyupdates go to contacts who plausibly hold our key:
# an accepted chat alone is not enough, a message must have flowed.
alice_chat = alice_contact_bob.create_chat()
alice.set_config("keyupdate_debounce", "1")
old_addr = alice.get_config("configured_addr")
alice_chat.send_text("hi")
assert remote_eval("bob.wait_for_incoming_msg().get_snapshot().text") == "hi"
before = bob_sees()
# Certificate merging keeps the newest direct key signature,
# and signature timestamps have one-second resolution:
# without waiting, the re-signed key can tie with the copy Bob holds, keeping his.
time.sleep(2)
alice.add_transport_from_qr(acf.get_account_qr())
(new_addr,) = [t["addr"] for t in alice.list_transports() if t["addr"] != old_addr]
if replace_relay:
alice.delete_transport(old_addr)
alice.bring_online()
# The 2.48 core has no encryption enforcement, but the keyupdate MDN without
# referenced message keeps it invisible; merging happens before the trashing.
for _ in range(60):
if new_addr in remote_eval("bob_contact_alice.get_encryption_info()"):
break
time.sleep(1)
else:
pytest.fail("Bob never received the keyupdate")
# It also leaves no trace: no chat with Alice, no message anywhere,
# and no address-contact for the address it was sent from.
assert bob_sees() == before
if replace_relay:
remote_eval("bob_contact_alice.create_chat().send_text('hello after replacement')")
assert alice.wait_for_incoming_msg().get_snapshot().text == "hello after replacement"
class LocalSide:
"""Alice on the core under test."""
def __init__(self, account, peer_contact):
self.account = account
self.peer_contact = peer_contact
self.chat = None
def make_qr(self, invite):
if invite == "group":
self.chat = self.account.create_group("Group")
elif invite == "broadcast":
self.chat = self.account.create_broadcast("Channel")
return self.chat.get_qr_code() if self.chat else self.account.get_qr_code()
def join(self, qr):
self.account.secure_join(qr)
def wait_inviter(self):
self.account.wait_for_securejoin_inviter_success()
def wait_joiner(self):
self.account.wait_for_securejoin_joiner_success()
def send_text(self, text):
chat = self.chat or self.peer_contact.create_chat()
chat.send_text(text)
def next_text(self):
return self.account.wait_for_incoming_msg().get_snapshot().text
class RemoteSide:
"""Bob on the other core, driven through remote_eval."""
def __init__(self, remote_eval):
self.remote_eval = remote_eval
self.chat = None
def make_qr(self, invite):
if invite == "contact":
return self.remote_eval("bob.get_qr_code()")
create = {"group": "bob.create_group('Group')", "broadcast": "bob.create_broadcast('Channel')"}[invite]
self.remote_eval(f"locals().update(chat={create})")
self.chat = "chat"
return self.remote_eval("chat.get_qr_code()")
def join(self, qr):
self.remote_eval(f"bob.secure_join({qr!r})")
def wait_inviter(self):
self.remote_eval("bob.wait_for_securejoin_inviter_success()")
def wait_joiner(self):
self.remote_eval("bob.wait_for_securejoin_joiner_success()")
def send_text(self, text):
chat = self.chat or "bob_contact_alice.create_chat()"
self.remote_eval(f"{chat}.send_text({text!r})")
def next_text(self):
return self.remote_eval("bob.wait_for_incoming_msg().get_snapshot().text")
@pytest.mark.parametrize("version", ["2.48.0"])
@pytest.mark.parametrize("invite", ["contact", "group", "broadcast"])
@pytest.mark.parametrize("remote_invites", [False, True], ids=["local-invites", "remote-invites"])
def test_securejoin_invite(alice_and_remote_bob, version, invite, remote_invites):
"""Every invite link type works with either core as the inviter."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
local = LocalSide(alice, alice_contact_bob)
remote = RemoteSide(remote_eval)
inviter, joiner = (remote, local) if remote_invites else (local, remote)
joiner.join(inviter.make_qr(invite))
joiner.wait_joiner()
inviter.wait_inviter()
# Group and broadcast joins add an info message first.
if invite != "contact":
joiner.next_text()
inviter.send_text("hello")
assert joiner.next_text() == "hello"

View File

@@ -5,12 +5,12 @@ from imap_tools import AND, U
from deltachat_rpc_client import EventType
def test_moved_markseen(acf, direct_imap, log):
def test_moved_markseen(acfactory, direct_imap, log):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1 = acf.get_online_account()
ac1 = acfactory.get_online_account()
addr, password = acf.get_credentials()
ac2 = acf.get_unconfigured_account()
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.bring_online()
@@ -57,28 +57,24 @@ def test_moved_markseen(acf, direct_imap, log):
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
def test_markseen_message_and_mdn(acf, direct_imap):
ac1, ac2 = acf.get_online_accounts(2)
def test_markseen_message_and_mdn(acfactory, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
# Make sure that messages are not immediately auto-deleted on the server:
ac1.set_config("bcc_self", "1")
ac2.set_config("bcc_self", "1")
acf.get_accepted_chat(ac1, ac2).send_text("hi")
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2.wait_for_incoming_msg()
msg.mark_seen()
rex = re.compile("Marked messages ([0-9,:]+) in folder INBOX as seen.")
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
# Each profile flags two messages but the logged UID set
# covers a varying number of them, so just count UIDs mentioned.
# We are not processing UID ranges, here we just care for two UIDs.
for ac in ac1, ac2:
uids = set()
while len(uids) < 2:
while True:
event = ac.wait_for_event()
if event.kind == EventType.INFO and (match := rex.search(event.msg)):
uids.update(re.split("[,:]", match.group(1)))
if event.kind == EventType.INFO and rex.search(event.msg):
break
ac1_direct_imap = direct_imap(ac1)
ac2_direct_imap = direct_imap(ac2)
@@ -91,8 +87,8 @@ def test_markseen_message_and_mdn(acf, direct_imap):
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
def test_trash_multiple_messages(acf, direct_imap, log):
ac1, ac2 = acf.get_online_accounts(2)
def test_trash_multiple_messages(acfactory, direct_imap, log):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.stop_io()
# Make sure that messages are not immediately auto-deleted on the server:
@@ -101,7 +97,7 @@ def test_trash_multiple_messages(acf, direct_imap, log):
ac2.set_config("sync_msgs", "0")
ac2.start_io()
chat12 = acf.get_accepted_chat(ac1, ac2)
chat12 = acfactory.get_accepted_chat(ac1, ac2)
log.section("ac1: sending 3 messages")
texts = ["first", "second", "third"]

View File

@@ -7,24 +7,20 @@ If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import itertools
import logging
import os
import threading
from contextlib import contextmanager
import time
import pytest
from deltachat_rpc_client import EventType
# Relays on underscore domains advertise themselves as iroh relay
# but serve a self-signed certificate that iroh's TLS stack rejects.
# Skipping instead of xfailing keeps the run fast:
# these tests only fail after waiting for realtime connections to time out.
pytestmark = pytest.mark.skipif(
os.environ.get("CHATMAIL_DOMAIN", "").startswith("_"),
reason="iroh does not accept the self-signed certificate of an underscore domain",
)
@pytest.fixture(autouse=True)
def _xfail_underscore_domain():
if os.environ.get("CHATMAIL_DOMAIN", "").startswith("_"):
pytest.xfail("iroh does not support underscore domains")
@pytest.fixture
@@ -45,11 +41,7 @@ def log(msg):
logging.info(msg)
# payload used to probe/establish realtime connectivity, filtered out by tests
SETUP_DATA = b"realtime-setup"
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
assert ac1.get_config("webxdc_realtime_enabled") == "1"
assert ac2.get_config("webxdc_realtime_enabled") == "1"
ac1_ac2_chat = ac1.create_chat(ac2)
@@ -66,44 +58,45 @@ def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
if wait:
wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg)])
return ac1_webxdc_msg, ac2_webxdc_msg
@contextmanager
def send_realtime_data_forever(msgs, data=None):
stop = threading.Event()
data = data or [SETUP_DATA] * len(msgs)
def setup_thread_send_realtime_data(msg, data):
def thread_run():
for _i in range(10):
msg.send_webxdc_realtime_data(data)
time.sleep(1)
def thread_run(msg, payload):
for i in itertools.count():
msg.send_webxdc_realtime_data(payload(i) if callable(payload) else payload)
if stop.wait(1):
return
for msg_payload in zip(msgs, data, strict=True):
threading.Thread(target=thread_run, args=msg_payload, daemon=True).start()
try:
yield
finally:
stop.set()
threading.Thread(target=thread_run, daemon=True).start()
def wait_realtime_connected(msg_pairs):
with send_realtime_data_forever([sender for sender, _ in msg_pairs]):
for _, receiver in msg_pairs:
receiver.account.wait_for_realtime_data(receiver.id)
def wait_receive_realtime_data(msg_data_list):
account = msg_data_list[0][0].account
msg_data_list = msg_data_list[:]
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
while msg_data_list:
event = account.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
for i, (msg, data) in enumerate(msg_data_list):
if msg.id == event.msg_id:
assert list(data) == event.data
log(f"msg {msg.id}: got correct realtime data {data}")
del msg_data_list[i]
break
def test_realtime_sequentially(acf, path_to_webxdc):
def test_realtime_sequentially(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection sequentially."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1.create_chat(ac2)
ac2.create_chat(ac1)
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = acf.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
snapshot = ac2_webxdc_msg.get_snapshot()
assert snapshot.text == "play"
@@ -111,7 +104,7 @@ def test_realtime_sequentially(acf, path_to_webxdc):
# send iroh announcements sequentially
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
acf.send_message(from_account=ac1, to_account=ac2, text="ping1")
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
log("waiting for incoming message on ac2")
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
@@ -119,7 +112,7 @@ def test_realtime_sequentially(acf, path_to_webxdc):
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
acf.send_message(from_account=ac2, to_account=ac1, text="ping2")
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
log("waiting for incoming message on ac1")
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
@@ -130,27 +123,50 @@ def test_realtime_sequentially(acf, path_to_webxdc):
data = os.urandom(128000)
ac1_webxdc_msg.send_webxdc_realtime_data(data)
assert ac2.wait_for_realtime_data(ac2_webxdc_msg.id) == data
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == list(data)
break
def test_realtime_simultaneously(acf, path_to_webxdc):
def test_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acf.get_online_accounts(2)
setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
def test_two_parallel_realtime_simultaneously(acf, path_to_webxdc):
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acf.get_online_accounts(2)
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False)
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=False)
wait_realtime_connected([(ac1_webxdc_msg, ac2_webxdc_msg), (ac2_webxdc_msg, ac1_webxdc_msg)])
wait_realtime_connected([(ac1_webxdc_msg2, ac2_webxdc_msg2), (ac2_webxdc_msg2, ac1_webxdc_msg2)])
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
def test_no_duplicate_messages(acf, path_to_webxdc):
def test_no_duplicate_messages(acfactory, path_to_webxdc):
"""Test that messages are received only once."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
@@ -164,29 +180,50 @@ def test_no_duplicate_messages(acf, path_to_webxdc):
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
with send_realtime_data_forever([ac1_webxdc_msg], data=[lambda i: str(i).encode()]):
n = int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode())
assert int(ac2.wait_for_realtime_data(ac2_webxdc_msg.id).decode()) > n
def thread_run():
for i in range(10):
data = str(i).encode()
ac1_webxdc_msg.send_webxdc_realtime_data(data)
time.sleep(1)
threading.Thread(target=thread_run, daemon=True).start()
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
n = int(bytes(event.data).decode())
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
assert int(bytes(event.data).decode()) > n
def test_no_reordering(acf, path_to_webxdc):
def test_no_reordering(acfactory, path_to_webxdc):
"""Test that sending a lot of realtime messages does not result in reordering."""
ac1, ac2 = acf.get_online_accounts(2)
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, b"hello")
wait_receive_realtime_data([(ac2_webxdc_msg, b"hello")])
for i in range(200):
ac1_webxdc_msg.send_webxdc_realtime_data([i])
for i in range(200):
# lingering SETUP_DATA payloads from the wait_realtime_connected() barrier may still arrive
while (data := ac2.wait_for_realtime_data(ac2_webxdc_msg.id)) == SETUP_DATA:
pass
assert data == bytes([i]), "Reordering detected"
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA and bytes(event.data) != b"hello":
if event.data[0] == i:
break
pytest.fail("Reordering detected")
def test_advertisement_after_chatting(acf, path_to_webxdc):
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
"""Test that realtime advertisement is assigned to the correct message after chatting."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
@@ -205,14 +242,17 @@ def test_advertisement_after_chatting(acf, path_to_webxdc):
assert event.msg_id == ac1_webxdc_msg.id
def test_realtime_large_webxdc(acf, path_to_large_webxdc):
def test_realtime_large_webxdc(acfactory, path_to_large_webxdc):
"""Tests initializing realtime channel on a large webxdc.
This is a regression test for a bug that existed in version 2.42.0.
Large webxdc is split into pre- and post- message,
and this previously resulted in failure to initialize realtime.
"""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac2.create_chat(ac1)
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="realtime check", file=path_to_large_webxdc)

View File

@@ -1,15 +1,15 @@
def test_set_location(dc, acf) -> None:
def test_set_location(dc, acfactory) -> None:
# Try setting location without any accounts.
assert not dc.set_location(1.0, 2.0, 0.1)
# Create one account that does not stream,
# set location.
acf.new_configured_account()
acfactory.new_configured_account()
assert not dc.set_location(3.0, 4.0, 0.1)
def test_send_locations_to_chat(dc, acf):
alice, bob = acf.get_online_accounts(2)
def test_send_locations_to_chat(dc, acfactory):
alice, bob = acfactory.get_online_accounts(2)
assert not alice.is_sending_locations()
alice_chat_bob = alice.create_chat(bob)

View File

@@ -4,8 +4,8 @@ from deltachat_rpc_client import EventType
from deltachat_rpc_client.const import MessageState
def test_bcc_self_is_enabled_when_setting_up_second_device(acf):
ac = acf.get_online_account()
def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
ac = acfactory.get_online_account()
# Initially after getting online
# the setting bcc_self is set to 0 because there is only one device
@@ -29,8 +29,8 @@ def test_bcc_self_is_enabled_when_setting_up_second_device(acf):
assert ac.get_config("bcc_self") == "1"
def test_one_account_send_bcc_setting(acf, log, direct_imap):
ac1, ac2 = acf.get_online_accounts(2)
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
@@ -75,9 +75,9 @@ def test_one_account_send_bcc_setting(acf, log, direct_imap):
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_multidevice_sync_seen(acf, log):
def test_multidevice_sync_seen(acfactory, log):
"""Test that message marked as seen on one device is marked as seen on another."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
@@ -127,34 +127,3 @@ def test_multidevice_sync_seen(acf, log):
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
# Test that the timer is started on the second device after synchronizing the seen status.
assert "Expires: " in ac1_clone_message.get_info()
def test_multidevice_sync_seen_mdns_off(acf, log):
"""Test that MDNs to self are sent even if MDNs are disabled."""
ac1, ac2 = acf.get_online_accounts(2)
ac1.set_config("mdns_enabled", "0")
ac1_clone = ac1.clone()
ac1_clone.bring_online()
assert ac1.get_config("bcc_self") == "1"
assert ac1.get_config("mdns_enabled") == "0"
assert ac1_clone.get_config("bcc_self") == "1"
assert ac1_clone.get_config("mdns_enabled") == "0"
ac1.create_chat(ac2)
ac1_clone_chat = ac1_clone.create_chat(ac2)
ac2_chat = ac2.create_chat(ac1)
log.section("Send a message from ac2 to ac1 and check that it's 'fresh'")
ac2_chat.send_text("Hi")
ac1_message = ac1.wait_for_incoming_msg()
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
ac1_message.mark_seen()
assert ac1_message.get_snapshot().state == MessageState.IN_SEEN
log.section("ac1 clone detects that message is marked as seen")
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
assert ev.chat_id == ac1_clone_chat.id
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN

View File

@@ -1,6 +1,3 @@
import time
import urllib.parse
import pytest
from deltachat_rpc_client import EventType
@@ -8,27 +5,11 @@ from deltachat_rpc_client.const import ChatType, DownloadState
from deltachat_rpc_client.rpc import JsonRpcError
def alice_with_two_transports_and_bob(acf):
alice, bob = acf.get_online_accounts(2)
alice.add_transport_from_qr(acf.get_account_qr())
alice.bring_online()
return alice, alice.create_chat(bob), bob.create_chat(alice)
def messages_with_text(chat, text):
return [msg for msg in chat.get_messages() if msg.get_snapshot().text == text]
def wait_for_imap_message(imap):
while not imap.get_all_messages():
time.sleep(1)
def test_add_second_address(acf) -> None:
account = acf.new_configured_account()
def test_add_second_address(acfactory) -> None:
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 2
@@ -37,23 +18,18 @@ def test_add_second_address(acf) -> None:
first_addr = account.list_transports()[0]["addr"]
second_addr = account.list_transports()[1]["addr"]
third_addr = account.list_transports()[2]["addr"]
assert account.get_config("configured_addr") == first_addr
account.delete_transport(first_addr)
assert len(account.list_transports()) == 2
assert account.get_config("configured_addr") != first_addr
# Cannot delete the first address.
with pytest.raises(JsonRpcError):
account.delete_transport(first_addr)
account.delete_transport(second_addr)
assert len(account.list_transports()) == 1
with pytest.raises(JsonRpcError):
account.delete_transport(third_addr)
assert len(account.list_transports()) == 2
def test_change_address(acf) -> None:
def test_change_address(acfactory) -> None:
"""Test Alice configuring a second transport and setting it as a primary one."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("configured_addr")
bob.create_chat(alice)
@@ -68,7 +44,7 @@ def test_change_address(acf) -> None:
old_alice_addr = alice.get_config("configured_addr")
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr in alice_vcard
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
with pytest.raises(JsonRpcError):
@@ -85,6 +61,8 @@ def test_change_address(acf) -> None:
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr not in alice_vcard
assert new_alice_addr in alice_vcard
with pytest.raises(JsonRpcError):
alice.delete_transport(new_alice_addr)
alice.start_io()
alice_chat_bob.send_text("Hello again!")
@@ -98,37 +76,18 @@ def test_change_address(acf) -> None:
assert sender_addr2 == new_alice_addr
def test_remove_transport_keep_messages(acf) -> None:
"""Test that deleting current sending transport keeps queued messages."""
alice, bob = acf.get_online_accounts(2)
qr = acf.get_account_qr()
alice.add_transport_from_qr(qr)
alice.stop_io()
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello!")
new_alice_addr = alice.list_transports()[1]["addr"]
alice.delete_transport(alice.list_transports()[0]["addr"])
alice.start_io()
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
assert bob_msg.sender.get_snapshot().address == new_alice_addr
def test_download_on_demand(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
def test_download_on_demand(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice.set_config("download_limit", "1")
alice.stop_io()
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
alice.start_io()
alice.create_chat(bob)
chat_bob_alice = bob.create_chat(alice)
chat_bob_alice.send_message(file=rpcdata.get_path("image/screenshot.jpg"))
chat_bob_alice.send_message(file=data.get_path("image/screenshot.jpg"))
msg = alice.wait_for_incoming_msg()
snapshot = msg.get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
@@ -144,15 +103,15 @@ def test_download_on_demand(acf, rpcdata) -> None:
assert msg.get_snapshot().download_state == dstate
def test_reconfigure_transport(acf) -> None:
def test_reconfigure_transport(acfactory) -> None:
"""Test that reconfiguring the transport works."""
account = acf.get_online_account()
account = acfactory.get_online_account()
[transport] = account.list_transports()
account.add_or_update_transport(transport)
def test_transport_synchronization(acf, log) -> None:
def test_transport_synchronization(acfactory, log) -> None:
"""Test synchronization of transports between devices."""
def wait_for_io_started(ac):
@@ -161,24 +120,22 @@ def test_transport_synchronization(acf, log) -> None:
if "scheduler is running" in ev.msg:
return
def wait_transports(ac, n):
while len(ac.list_transports()) != n:
ac.wait_for_event(EventType.TRANSPORTS_MODIFIED)
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
ac1.add_transport_from_qr(qr)
wait_transports(ac1_clone, 2)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
wait_for_io_started(ac1_clone)
assert len(ac1.list_transports()) == 2
assert len(ac1_clone.list_transports()) == 2
ac1_clone.add_transport_from_qr(qr)
wait_transports(ac1, 3)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
wait_for_io_started(ac1)
assert len(ac1.list_transports()) == 3
assert len(ac1_clone.list_transports()) == 3
log.section("ac1 clone removes second transport")
@@ -186,17 +143,24 @@ def test_transport_synchronization(acf, log) -> None:
addr3 = transport3["addr"]
ac1_clone.delete_transport(transport2["addr"])
wait_transports(ac1, 2)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
wait_for_io_started(ac1)
[transport1, transport3] = ac1.list_transports()
log.section("ac1 changes the sending transport")
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport3["addr"])
# One event for updated `add_timestamp` of the new primary transport,
# one event for the `configured_addr` update.
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport1, transport3] = ac1_clone.list_transports()
assert ac1_clone.get_config("configured_addr") == addr3
log.section("ac1 removes the first transport")
ac1.delete_transport(transport1["addr"])
wait_transports(ac1_clone, 1)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
wait_for_io_started(ac1_clone)
[transport3] = ac1_clone.list_transports()
assert transport3["addr"] == addr3
@@ -209,16 +173,15 @@ def test_transport_synchronization(acf, log) -> None:
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
def test_transport_sync_new_as_primary(acf, log) -> None:
"""Test that a transport promoted on one device is usable on other devices."""
ac1, bob = acf.get_online_accounts(2)
def test_transport_sync_new_as_primary(acfactory, log) -> None:
"""Test synchronization of new transport as primary between devices."""
ac1, bob = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
ac1.add_transport_from_qr(qr)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
ac1_transports = ac1.list_transports()
assert len(ac1_transports) == 2
[transport1, transport2] = ac1_transports
@@ -229,7 +192,10 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport2["addr"])
log.section("ac1_clone receives a message via the new transport")
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
log.section("ac1_clone receives a message via the new primary transport")
ac1_chat = ac1.create_chat(bob)
ac1_chat.send_text("Hello!")
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
@@ -239,12 +205,12 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
def test_recognize_self_address(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_recognize_self_address(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_chat = bob.create_chat(alice)
qr = acf.get_account_qr()
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
@@ -255,10 +221,10 @@ def test_recognize_self_address(acf) -> None:
assert msg.chat == alice.create_chat(bob)
def test_transport_limit(acf) -> None:
def test_transport_limit(acfactory) -> None:
"""Test transports limit."""
account = acf.get_online_account()
qr = acf.get_account_qr()
account = acfactory.get_online_account()
qr = acfactory.get_account_qr()
limit = 5
@@ -271,22 +237,33 @@ def test_transport_limit(acf) -> None:
account.add_transport_from_qr(qr)
second_addr = account.list_transports()[1]["addr"]
third_addr = account.list_transports()[2]["addr"]
account.delete_transport(second_addr)
# test that adding a transport after unpublishing one works again
account.set_transport_unpublished(second_addr)
account.add_transport_from_qr(qr)
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
# UIs are not expected to delete transports directly,
# but we still test that adding a transport
# after deleting one instead of unpublishing works.
account.delete_transport(third_addr)
account.add_transport_from_qr(qr)
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
def test_message_info_imap_urls(acf) -> None:
def test_message_info_imap_urls(acfactory) -> None:
"""Test that message info contains IMAP URLs of where the message was received."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
qr = acf.get_account_qr()
for _ in range(3):
qr = acfactory.get_account_qr()
for i in range(3):
alice.add_transport_from_qr(qr)
# Wait for all transports to go IDLE after adding each one.
alice.bring_online()
for _ in range(i + 1):
alice.bring_online()
# Enable multi-device mode so messages are not deleted immediately.
alice.set_config("bcc_self", "1")
@@ -316,12 +293,20 @@ def test_message_info_imap_urls(acf) -> None:
assert f"{new_alice_addr}/INBOX" in msg_info
def test_remove_primary_transport(acf, log) -> None:
def test_remove_primary_transport(acfactory, log) -> None:
"""Test that after removing the primary relay, Alice can still receive messages."""
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
alice, bob = acfactory.get_online_accounts(2)
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
alice.bring_online()
bob_chat = bob.create_chat(alice)
alice.create_chat(bob)
log.section("Alice sets up second transport")
[transport1, transport2] = alice.list_transports()
alice.set_config("configured_addr", transport2["addr"])
bob_chat.send_text("Hello!")
msg1 = alice.wait_for_incoming_msg().get_snapshot()
@@ -329,7 +314,6 @@ def test_remove_primary_transport(acf, log) -> None:
log.section("Alice removes the primary relay")
alice.delete_transport(transport1["addr"])
assert alice.get_config("configured_addr") == transport2["addr"]
alice.stop_io()
alice.start_io()
@@ -337,64 +321,4 @@ def test_remove_primary_transport(acf, log) -> None:
msg2 = alice.wait_for_incoming_msg().get_snapshot()
assert msg2.text == "Hello again!"
assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE
assert msg2.chat == alice_chat
def test_qr_works_after_removing_primary_transport(acf, log) -> None:
log.section("Alice setups an account and adds two additional relays")
alice = acf.new_configured_account()
relay_qr = acf.get_account_qr()
alice.add_transport_from_qr(relay_qr)
alice.add_transport_from_qr(relay_qr)
first_addr = alice.list_transports()[0]["addr"]
second_addr = alice.list_transports()[1]["addr"]
third_addr = alice.list_transports()[2]["addr"]
log.section("Alice creates a QR code")
chat_qr = alice.get_qr_code()
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
assert f"&a={first_addr}" in chat_qr_unquoted
assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted
log.section("Alice removes first and second transport")
alice.set_config("configured_addr", third_addr)
alice.delete_transport(first_addr)
alice.delete_transport(second_addr)
log.section("Bob scans the QR code, which still works")
alice.bring_online()
bob = acf.get_online_account()
bob.secure_join(chat_qr)
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
def test_background_fetch_from_second_transport(acf, direct_imap, dc):
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
[transport1, transport2] = alice.list_transports()
assert alice.get_config("configured_addr") == transport1["addr"]
alice.stop_io()
bob_chat.send_text("hello")
imap1 = direct_imap(alice, transport1["addr"], transport1["password"])
wait_for_imap_message(direct_imap(alice, transport2["addr"], transport2["password"]))
wait_for_imap_message(imap1)
# Leave the message on the second transport only.
imap1.delete("1:*")
dc.background_fetch(300)
assert len(messages_with_text(alice_chat, "hello")) == 1
def test_background_fetch_no_duplicates(acf, direct_imap, dc):
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
alice.stop_io()
bob_chat.send_text("hello")
for transport in alice.list_transports():
wait_for_imap_message(direct_imap(alice, transport["addr"], transport["password"]))
dc.background_fetch(300)
assert len(messages_with_text(alice_chat, "hello")) == 1
assert msg2.chat == alice.create_chat(bob)

View File

@@ -4,29 +4,48 @@ import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
from deltachat_rpc_client.const import ChatType
from deltachat_rpc_client.rpc import JsonRpcError
def test_qr_setup_contact(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.e2ee_avail
assert alice_contact_bob_snapshot.is_verified
bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.e2ee_avail
assert bob_contact_alice_snapshot.is_verified
# Test that if Bob imports a key,
# backwards verification is not lost
# because default key is not changed.
logging.info("Bob 2 is created")
bob2 = acfactory.new_configured_account()
bob2.export_self_keys(tmp_path)
logging.info("Bob tries to import a key")
# Importing a second key is not allowed.
with pytest.raises(JsonRpcError):
bob.import_self_keys(tmp_path)
assert bob.get_config("key_id") == "1"
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
def test_qr_setup_contact_svg(acf) -> None:
alice = acf.new_configured_account()
def test_qr_setup_contact_svg(acfactory) -> None:
alice = acfactory.new_configured_account()
_, _, domain = alice.get_config("addr").rpartition("@")
_qr_code, svg = alice.get_qr_code_svg()
@@ -40,8 +59,8 @@ def test_qr_setup_contact_svg(acf) -> None:
assert "Alice" in svg
def test_qr_securejoin(acf):
alice, bob, fiona = acf.get_online_accounts(3)
def test_qr_securejoin(acfactory):
alice, bob, fiona = acfactory.get_online_accounts(3)
# Setup second device for Alice
# to test observing securejoin protocol.
@@ -62,24 +81,26 @@ def test_qr_securejoin(acf):
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.e2ee_avail
assert alice_contact_bob_snapshot.is_verified
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.e2ee_avail
assert bob_contact_alice_snapshot.is_verified
# Start second Alice device.
# Alice observes the securejoin protocol on the second device.
# Alice observes securejoin protocol and verifies Bob on second device.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.create_contact(bob)
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.e2ee_avail
assert alice2_contact_bob_snapshot.is_verified
# The QR code token is synced, so alice2 must be able to handle join requests.
logging.info("Fiona joins the group via alice2")
@@ -90,8 +111,8 @@ def test_qr_securejoin(acf):
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_qr_securejoin_broadcast(acf, all_devices_online):
alice, bob, fiona = acf.get_online_accounts(3)
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
alice, bob, fiona = acfactory.get_online_accounts(3)
alice2 = alice.clone()
bob2 = bob.clone()
@@ -130,9 +151,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
assert snapshot2.chat_id == chat.id
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
# Check that the chat partner's key is known.
# Check that the chat partner is verified.
contact_snapshot = contact.get_snapshot()
assert contact_snapshot.e2ee_avail
assert contact_snapshot.is_verified
chat = get_broadcast(ac)
chat_msgs = chat.get_messages()
@@ -231,9 +252,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
def test_qr_securejoin_contact_request(acf) -> None:
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -257,8 +278,8 @@ def test_qr_securejoin_contact_request(acf) -> None:
assert bob_chat_alice.get_basic_snapshot().is_contact_request
def test_qr_readreceipt(acf) -> None:
alice, bob, charlie = acf.get_online_accounts(3)
def test_qr_readreceipt(acfactory) -> None:
alice, bob, charlie = acfactory.get_online_accounts(3)
logging.info("Bob and Charlie setup contact with Alice")
qr_code = alice.get_qr_code()
@@ -314,24 +335,24 @@ def test_qr_readreceipt(acf) -> None:
assert not bob.get_chat_by_contact(bob_contact_charlie)
def test_setup_contact_resetup(acf) -> None:
def test_setup_contact_resetup(acfactory) -> None:
"""Tests that setup contact works after Alice resets the device and changes the key."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
alice = acf.resetup_account(alice)
alice = acfactory.resetup_account(alice)
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
def test_group_member_added_recovery(acf) -> None:
"""Tests group recovery after a member resets its key."""
ac1, ac2, ac3 = acf.get_online_accounts(3)
def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates a group")
chat = ac1.create_group("Group")
@@ -341,7 +362,11 @@ def test_group_member_added_recovery(acf) -> None:
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 joins the group")
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.create_contact(ac2)
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
@@ -349,9 +374,9 @@ def test_group_member_added_recovery(acf) -> None:
ac3_contact_ac2_old = ac3.create_contact(ac2)
logging.info("ac2 logs in on a new device")
ac2 = acf.resetup_account(ac2)
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 scans ac3's QR code again")
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -391,20 +416,28 @@ def test_group_member_added_recovery(acf) -> None:
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Works again!"
ac1_contact_ac2 = ac1.create_contact(ac2)
ac1_contact_ac3 = ac1.create_contact(ac3)
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
issue <https://github.com/chatmail/core/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acf.get_online_accounts(4)
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
logging.info("ac3: set up contact with ac2")
logging.info("ac3: verify with ac2")
qr_code = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
# in order for ac2 to have pending bobstate with a group
# we first create a fully joined group, and then start
# in order for ac2 to have pending bobstate with a verified group
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
logging.info("ac1: create a group that ac2 fully joins")
@@ -413,7 +446,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
# ensure ac1 can write and ac2 receives messages in the chat
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
@@ -426,11 +459,11 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect the ac3/ac2 setup-contact started in the beginning to have completed
assert ac3.create_contact(ac2).get_snapshot().e2ee_avail
assert ac2.create_contact(ac3).get_snapshot().e2ee_avail
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.create_contact(ac2).get_snapshot().is_verified
assert ac2.create_contact(ac3).get_snapshot().is_verified
logging.info("ac3: create a group VG with ac2")
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created")
vg.add_contact(ac3.create_contact(ac2))
@@ -451,9 +484,9 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
return
def test_qr_new_group_unblocked(acf):
def test_qr_new_group_unblocked(acfactory):
"""Regression test for a bug introduced in core v1.113.0.
ac2 scans a group QR code created by ac1.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked single 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.
@@ -461,7 +494,7 @@ def test_qr_new_group_unblocked(acf):
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining")
qr_code = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
@@ -480,13 +513,13 @@ def test_qr_new_group_unblocked(acf):
@pytest.mark.skip(reason="AEAP is disabled for now")
def test_aeap_flow(acf):
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
addr, password = acf.get_credentials()
addr, password = acfactory.get_credentials()
logging.info("ac1: create group QR, ac2 scans and joins")
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello")
qr_code = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
@@ -522,15 +555,65 @@ def test_aeap_flow(acf):
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
def test_securejoin_after_contact_resetup(acf) -> None:
"""
Regression test for a bug that prevented joining a group with a QR code
if the group already contains a contact with the same address as the inviter,
but different key fingerprint while a securejoin with that contact is still pending.
"""
ac1, ac2, ac3 = acf.get_online_accounts(3)
def test_gossip_verification(acfactory) -> None:
alice, bob, carol = acfactory.get_online_accounts(3)
# ac3 creates a group with ac1.
# Bob verifies Alice.
qr_code = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
# Bob verifies Carol.
qr_code = carol.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
bob_contact_alice = bob.create_contact(alice, "Alice")
bob_contact_carol = bob.create_contact(carol, "Carol")
carol_contact_alice = carol.create_contact(alice, "Alice")
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Autocrypt group")
snapshot = carol.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Autocrypt group"
assert snapshot.show_padlock
# Group propagates verification using Autocrypt-Gossip header.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not carol_contact_alice_snapshot.is_verified
logging.info("Bob creates a Securejoin group")
bob_group_chat = bob.create_group("Securejoin Group")
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Securejoin group")
snapshot = carol.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello Securejoin group"
assert snapshot.show_padlock
# Securejoin propagates verification.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not carol_contact_alice_snapshot.is_verified
def test_securejoin_after_contact_resetup(acfactory) -> None:
"""
Regression test for a bug that prevented joining verified group with a QR code
if the group is already created and contains
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac3 creates protected group with ac1.
ac3_chat = ac3.create_group("Group")
# ac1 joins ac3 group.
@@ -540,27 +623,31 @@ def test_securejoin_after_contact_resetup(acf) -> None:
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 sets up contact with ac1
# ac2 verifies ac1
qr_code = ac1.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
# ac1 is verified for ac2.
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert ac2_contact_ac1.get_snapshot().e2ee_avail
assert ac2_contact_ac1.get_snapshot().is_verified
# ac1 resetups the account.
ac1 = acf.resetup_account(ac1)
ac1 = acfactory.resetup_account(ac1)
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline.
ac1.remove()
# Scanning a QR code creates a group with the inviter, here ac1.
# Normally the securejoin protocol
# would complete and "Member added" would arrive,
# but ac1 is offline so it never finishes.
# Scanning a QR code results in creating an unprotected group with an inviter.
# In this case inviter is ac1 which has an inconsistent key state.
# Normally inviter becomes verified as a result of Securejoin protocol
# and then the group chat becomes verified when "Member added" is received,
# but in this case ac1 is offline and this Securejoin process will never finish.
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
ac2.secure_join(ac1_qr_code)
@@ -577,13 +664,16 @@ def test_securejoin_after_contact_resetup(acf) -> None:
ac2_chat = snapshot.chat
assert len(ac2_chat.get_contacts()) == 3
# ac1 is still "not verified" for ac2 due to inconsistent state.
assert not ac2_contact_ac1.get_snapshot().is_verified
def test_withdraw_securejoin_qr(acf):
alice, bob = acf.get_online_accounts(2)
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group")
logging.info("Bob joins the group")
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code)
@@ -592,7 +682,7 @@ def test_withdraw_securejoin_qr(acf):
alice.clear_all_events()
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
@@ -616,8 +706,8 @@ def test_withdraw_securejoin_qr(acf):
break
def test_qr_scan_updates_new_relay_address(acf):
alice, bob = acf.get_online_accounts(2)
def test_qr_scan_updates_new_relay_address(acfactory):
alice, bob = acfactory.get_online_accounts(2)
bob_alice_chat = bob.secure_join(alice.get_qr_code())
alice.wait_for_securejoin_inviter_success()
@@ -625,7 +715,7 @@ def test_qr_scan_updates_new_relay_address(acf):
for ac in [alice, bob]:
old_addr = ac.get_config("configured_addr")
ac.add_transport_from_qr(acf.get_account_qr())
ac.add_transport_from_qr(acfactory.get_account_qr())
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
ac.delete_transport(old_addr)

View File

@@ -48,8 +48,8 @@ def test_email_address_validity(rpc) -> None:
assert not rpc.check_email_validity(addr)
def test_acf(acf) -> None:
account = acf.new_configured_account()
def test_acfactory(acfactory) -> None:
account = acfactory.new_configured_account()
while True:
event = account.wait_for_event()
if event.kind == EventType.CONFIGURE_PROGRESS:
@@ -61,9 +61,9 @@ def test_acf(acf) -> None:
logging.info("Successful configuration")
def test_configure_starttls(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
def test_configure_starttls(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -75,10 +75,10 @@ def test_configure_starttls(acf) -> None:
assert account.is_configured()
def test_lowercase_address(acf) -> None:
addr, password = acf.get_credentials()
def test_lowercase_address(acfactory) -> None:
addr, password = acfactory.get_credentials()
addr_upper = addr.upper()
account = acf.get_unconfigured_account()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr_upper,
@@ -103,10 +103,13 @@ def test_lowercase_address(acf) -> None:
assert addr_upper not in param
def test_configure_ip(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
def test_configure_ip(acfactory) -> None:
addr, password = acfactory.get_credentials()
domain = addr.rsplit("@")[-1]
if domain.startswith("_"):
pytest.skip("Underscore domains accept invalid certificates")
account = acfactory.get_unconfigured_account()
ip_address = socket.gethostbyname(domain)
with pytest.raises(JsonRpcError):
account.add_or_update_transport(
@@ -119,10 +122,10 @@ def test_configure_ip(acf) -> None:
)
def test_configure_alternative_port(acf) -> None:
def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -134,9 +137,9 @@ def test_configure_alternative_port(acf) -> None:
assert account.is_configured()
def test_list_transports(acf) -> None:
addr, password = acf.get_credentials()
account = acf.get_unconfigured_account()
def test_list_transports(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr,
@@ -152,8 +155,8 @@ def test_list_transports(acf) -> None:
assert params["imapUser"] == addr
def test_account(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -221,8 +224,8 @@ def test_account(acf) -> None:
alice.stop_io()
def test_mark_fresh_vs_self_mdn(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_mark_fresh_vs_self_mdn(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob.set_config("bcc_self", "1")
alice_contact_bob = alice.create_contact(bob)
@@ -245,8 +248,8 @@ def test_mark_fresh_vs_self_mdn(acf) -> None:
assert bob_chat.get_fresh_message_count() == 2
def test_chat(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -315,8 +318,8 @@ def test_chat(acf) -> None:
group.get_locations()
def test_contact(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -332,8 +335,8 @@ def test_contact(acf) -> None:
alice_contact_bob.create_chat()
def test_message(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -363,8 +366,8 @@ def test_message(acf) -> None:
assert reactions == snapshot.reactions
def test_receive_imf_failure(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_receive_imf_failure(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -392,8 +395,8 @@ def test_receive_imf_failure(acf) -> None:
assert snapshot.error is None
def test_selfavatar_sync(acf, rpcdata, log) -> None:
alice = acf.get_online_account()
def test_selfavatar_sync(acfactory, data, log) -> None:
alice = acfactory.get_online_account()
log.section("Alice adds a second device")
alice2 = alice.clone()
@@ -402,7 +405,7 @@ def test_selfavatar_sync(acf, rpcdata, log) -> None:
alice2.start_io()
log.section("First device changes avatar")
image = rpcdata.get_path("image/avatar1000x1000.jpg")
image = data.get_path("image/avatar1000x1000.jpg")
alice.set_config("selfavatar", image)
avatar_config = alice.get_config("selfavatar")
avatar_hash = os.path.basename(avatar_config)
@@ -417,10 +420,11 @@ def test_selfavatar_sync(acf, rpcdata, log) -> None:
assert avatar_config != avatar_config2
def test_dont_move_sync_msgs(acf, direct_imap):
addr, password = acf.get_credentials()
ac1 = acf.get_unconfigured_account()
def test_dont_move_sync_msgs(acfactory, direct_imap):
addr, password = acfactory.get_credentials()
ac1 = acfactory.get_unconfigured_account()
ac1.set_config("bcc_self", "1")
ac1.set_config("fix_is_chatmail", "1")
ac1.add_or_update_transport({"addr": addr, "password": password})
ac1.start_io()
ac1_direct_imap = direct_imap(ac1)
@@ -447,8 +451,8 @@ def test_dont_move_sync_msgs(acf, direct_imap):
time.sleep(1)
def test_reaction_seen_on_another_dev(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_reaction_seen_on_another_dev(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice2 = alice.clone()
alice2.start_io()
@@ -473,8 +477,8 @@ def test_reaction_seen_on_another_dev(acf) -> None:
assert chat_id == alice2_chat_bob.id
def test_2nd_device_events_when_msgs_are_seen(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice2 = alice.clone()
alice2.start_io()
@@ -502,9 +506,9 @@ def test_2nd_device_events_when_msgs_are_seen(acf) -> None:
assert chat_alice2.get_fresh_message_count() == 0
def test_is_bot(acf) -> None:
def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
@@ -518,18 +522,18 @@ def test_is_bot(acf) -> None:
assert snapshot.is_bot
def test_bot(acf) -> None:
def test_bot(acfactory) -> None:
mock = MagicMock()
user = (acf.get_online_accounts(1))[0]
bot = acf.new_configured_bot()
bot2 = acf.new_configured_bot()
user = (acfactory.get_online_accounts(1))[0]
bot = acfactory.new_configured_bot()
bot2 = acfactory.new_configured_bot()
assert bot.is_configured()
assert 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 = acf.process_message(from_account=user, to_client=bot, text="Hello!")
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = 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)
@@ -542,28 +546,28 @@ def test_bot(acf) -> None:
hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help"))
event = acf.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id)
event = acf.process_message(from_account=user, to_client=bot, text="hello!")
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
acf.process_message(from_account=bot2.account, to_client=bot, text="hello")
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
acf.process_message(from_account=user, to_client=bot, text="hey!")
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()
acf.process_message(from_account=user, to_client=bot, text="hello")
event = acf.process_message(from_account=user, to_client=bot, text="/help")
acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = 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(acf) -> None:
alice = acf.get_online_account()
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.get_online_account()
# Create a bot account so it does not receive device messages in the beginning.
addr, password = acf.get_credentials()
bot = acf.get_unconfigured_account()
addr, password = acfactory.get_credentials()
bot = acfactory.get_unconfigured_account()
bot.set_config("bot", "1")
bot.add_or_update_transport({"addr": addr, "password": password})
assert bot.is_configured()
@@ -589,19 +593,19 @@ def test_wait_next_messages(acf) -> None:
assert snapshot.text == "Hello!"
def test_import_export_backup(acf, tmp_path) -> None:
alice = acf.new_configured_account()
def test_import_export_backup(acfactory, tmp_path) -> None:
alice = acfactory.new_configured_account()
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acf.get_unconfigured_account()
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
assert alice2.manager.get_system_info()
def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
(ac1, some1) = acf.get_online_accounts(2)
def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
(ac1, some1) = acfactory.get_online_accounts(2)
log.section("create some chat content")
some1_addr = some1.get_config("addr")
@@ -609,7 +613,7 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
chat1.send_text("msg1")
assert len(ac1.get_contacts()) == 1
original_image_path = rpcdata.get_path("image/avatar64x64.png")
original_image_path = data.get_path("image/avatar64x64.png")
chat1.send_file(str(original_image_path))
# Add another 100KB file that ensures that the progress is smooth enough
@@ -660,7 +664,7 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
ac1.start_io()
log.section("get fresh empty account")
ac2 = acf.get_unconfigured_account()
ac2 = acfactory.get_unconfigured_account()
log.section("import backup and check it's proper")
ac2.import_backup(files_written[0])
@@ -697,8 +701,8 @@ def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
assert len(list(backupdir.glob("*.tar"))) == 2
def test_import_export_keys(acf, tmp_path) -> None:
alice, bob = acf.get_online_accounts(2)
def test_import_export_keys(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
@@ -710,7 +714,7 @@ def test_import_export_keys(acf, tmp_path) -> None:
alice_keys_path = tmp_path / "alice_keys"
alice_keys_path.mkdir()
alice.export_self_keys(alice_keys_path)
alice = acf.resetup_account(alice)
alice = acfactory.resetup_account(alice)
alice.import_self_keys(alice_keys_path)
snapshot.chat.accept()
@@ -745,14 +749,9 @@ def test_early_failure(tmp_path) -> None:
with pytest.raises(JsonRpcError, match="invalid_dir"):
rpc.start()
# Requests issued after the server exited must fail immediately
# instead of waiting forever for the finished reader loop.
with pytest.raises(JsonRpcError, match="RPC server closed"):
rpc.get_system_info()
def test_mdn_doesnt_break_autocrypt(acf) -> None:
alice, bob = acf.get_online_accounts(2)
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
@@ -782,10 +781,10 @@ def test_mdn_doesnt_break_autocrypt(acf) -> None:
@pytest.mark.parametrize("n_accounts", [3, 2])
def test_download_limit_chat_assignment(acf, tmp_path, n_accounts):
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
download_limit = 300000
alice, *others = acf.get_online_accounts(n_accounts)
alice, *others = acfactory.get_online_accounts(n_accounts)
bob = others[0]
alice_group = alice.create_group("test group")
@@ -821,10 +820,10 @@ def test_download_limit_chat_assignment(acf, tmp_path, n_accounts):
assert snapshot.chat == bob_group
def test_download_small_msg_first(acf, tmp_path):
def test_download_small_msg_first(acfactory, tmp_path):
download_limit = 70000
alice, bob0 = acf.get_online_accounts(2)
alice, bob0 = acfactory.get_online_accounts(2)
bob1 = bob0.clone()
bob1.set_config("download_limit", str(download_limit))
@@ -845,14 +844,14 @@ def test_download_small_msg_first(acf, tmp_path):
@pytest.mark.parametrize("delete_chat", [False, True])
def test_delete_available_msg(acf, tmp_path, direct_imap, delete_chat):
def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
"""
Tests `DownloadState.AVAILABLE` message deletion on the receiver side.
Also tests pre- and post-message deletion on the sender side.
"""
# Min. UI setting as of v2.35
download_limit = 163840
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
bob.set_config("download_limit", str(download_limit))
# Avoid immediate deletion from the server
alice.set_config("bcc_self", "1")
@@ -895,8 +894,8 @@ def test_delete_available_msg(acf, tmp_path, direct_imap, delete_chat):
break
def test_delete_fully_downloaded_msg(acf, tmp_path, direct_imap):
alice, bob = acf.get_online_accounts(2)
def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
alice, bob = acfactory.get_online_accounts(2)
# Avoid immediate deletion from the server
bob.set_config("bcc_self", "1")
@@ -931,8 +930,8 @@ def test_delete_fully_downloaded_msg(acf, tmp_path, direct_imap):
break
def test_imap_autodelete_fully_downloaded_msg(acf, tmp_path, direct_imap):
alice, bob = acf.get_online_accounts(2)
def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
alice, bob = acfactory.get_online_accounts(2)
chat_alice = alice.create_chat(bob)
path = tmp_path / "large"
@@ -960,12 +959,12 @@ def test_imap_autodelete_fully_downloaded_msg(acf, tmp_path, direct_imap):
break
def test_markseen_contact_request(acf):
def test_markseen_contact_request(acfactory):
"""
Test that seen status is synchronized for contact request messages
even though read receipt is not sent.
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
# Bob sets up a second device.
bob2 = bob.clone()
@@ -984,11 +983,11 @@ def test_markseen_contact_request(acf):
@pytest.mark.parametrize("team_profile", [True, False])
def test_no_markseen_in_team_profile(team_profile, acf):
def test_no_markseen_in_team_profile(team_profile, acfactory):
"""
Test that seen status is synchronized iff `team_profile` isn't set.
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
if team_profile:
bob.set_config("team_profile", "1")
@@ -1007,11 +1006,6 @@ def test_no_markseen_in_team_profile(team_profile, acf):
message.mark_seen()
# The MDN is queued in `smtp_mdns`, which is drained only after the regular
# `smtp` queue, so "Outgoing message" would otherwise overtake it on the wire.
# Wait for the read receipt to reach Alice before queueing "Outgoing message".
alice.wait_for_event(EventType.MSG_READ)
# Send a message and wait until it arrives
# in order to wait until Bob2 gets the markseen message.
# This also tests that outgoing messages
@@ -1029,11 +1023,11 @@ def test_no_markseen_in_team_profile(team_profile, acf):
assert message2.get_snapshot().state == MessageState.IN_SEEN
def test_read_receipt(acf):
def test_read_receipt(acfactory):
"""
Test sending a read receipt and ensure it is attributed to the correct contact.
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_contact_bob = alice.create_contact(bob)
@@ -1052,15 +1046,15 @@ def test_read_receipt(acf):
assert read_receipt_cnt == 1
def test_get_http_response(acf):
alice = acf.new_configured_account()
def test_get_http_response(acfactory):
alice = acfactory.new_configured_account()
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
assert http_response["mimetype"] == "text/html"
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
def test_configured_imap_certificate_checks(acf):
alice = acf.new_configured_account()
def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
# Certificate checks should be configured (not None)
assert "cert_strict" in alice.get_info().used_transport_settings
@@ -1079,8 +1073,8 @@ def test_configured_imap_certificate_checks(acf):
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
def test_no_old_msg_is_fresh(acf):
ac1, ac2 = acf.get_online_accounts(2)
def test_no_old_msg_is_fresh(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.start_io()
@@ -1107,9 +1101,9 @@ def test_no_old_msg_is_fresh(acf):
assert len(list(ac1.get_fresh_messages())) == 0
def test_rename_synchronization(acf):
def test_rename_synchronization(acfactory):
"""Test synchronization of contact renaming."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice2 = alice.clone()
alice2.bring_online()
@@ -1124,9 +1118,9 @@ def test_rename_synchronization(acf):
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
def test_rename_group(acf):
def test_rename_group(acfactory):
"""Test renaming the group."""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_group = alice.create_group("Test group")
alice_contact_bob = alice.create_contact(bob)
@@ -1155,8 +1149,8 @@ def test_get_all_accounts_deadlock(rpc):
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_leave_broadcast(acf, all_devices_online):
alice, bob = acf.get_online_accounts(2)
def test_leave_broadcast(acfactory, all_devices_online):
alice, bob = acfactory.get_online_accounts(2)
bob2 = bob.clone()
@@ -1256,8 +1250,8 @@ def test_leave_broadcast(acf, all_devices_online):
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
def test_leave_and_delete_group(acf, log):
alice, bob = acf.get_online_accounts(2)
def test_leave_and_delete_group(acfactory, log):
alice, bob = acfactory.get_online_accounts(2)
log.section("Alice creates a group")
alice_chat = alice.create_group("Group")
@@ -1280,12 +1274,12 @@ def test_leave_and_delete_group(acf, log):
alice.wait_for_event(EventType.CHAT_MODIFIED)
def test_immediate_autodelete(acf, direct_imap, log):
def test_immediate_autodelete(acfactory, direct_imap, log):
"""
`bcc_self` is off by default,
so that messages are supposed to be immediately autodeleted
"""
ac1, ac2 = acf.get_online_accounts(2)
ac1, ac2 = acfactory.get_online_accounts(2)
assert ac1.get_config("bcc_self") == "0"
log.section("ac1: create chat with ac2")
@@ -1316,8 +1310,8 @@ def test_immediate_autodelete(acf, direct_imap, log):
assert ev.msg_id == sent_msg.id
def test_background_fetch(acf, dc):
ac1, ac2 = acf.get_online_accounts(2)
def test_background_fetch(acfactory, dc):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.stop_io()
ac1_chat = ac1.create_chat(ac2)
@@ -1353,24 +1347,8 @@ def test_background_fetch(acf, dc):
break
def test_background_fetch_does_not_wait_for_sending(dc, acf):
alice, bob = acf.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice.stop_io()
text = "x" * 200_000
for _ in range(50):
alice_chat_bob.send_text(text)
assert not dc.is_sending_finished()
alice.start_io()
dc.background_fetch(50)
dc.wait_for_event(EventType.ACCOUNTS_BACKGROUND_FETCH_DONE)
assert not dc.is_sending_finished()
def test_message_exists(acf):
ac1, ac2 = acf.get_online_accounts(2)
def test_message_exists(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
message1 = chat.send_text("Hello!")
message2 = chat.send_text("Hello again!")
@@ -1388,7 +1366,7 @@ def test_message_exists(acf):
assert not message2.exists()
def test_synchronize_member_list_on_group_rejoin(acf, log):
def test_synchronize_member_list_on_group_rejoin(acfactory, log):
"""
Test that user recreates group member list when it joins the group again.
ac1 creates a group with two other accounts: ac2 and ac3
@@ -1396,7 +1374,7 @@ def test_synchronize_member_list_on_group_rejoin(acf, log):
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
"""
log.section("setting up accounts, accepted with each other")
ac1, ac2, ac3 = accounts = acf.get_online_accounts(3)
ac1, ac2, ac3 = accounts = acfactory.get_online_accounts(3)
log.section("ac1: creating group chat with 2 other members")
chat = ac1.create_group("title1")
@@ -1432,17 +1410,17 @@ def test_synchronize_member_list_on_group_rejoin(acf, log):
assert msg.get_snapshot().chat.num_contacts() == 2
def test_large_message(acf, rpcdata) -> None:
def test_large_message(acfactory, data) -> None:
"""
Test sending large message without download limit set,
so it is sent with pre-message but downloaded without user interaction.
"""
alice, bob = acf.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_message(
"Hello World, this message is bigger than 5 bytes",
file=rpcdata.get_path("image/screenshot.jpg"),
file=data.get_path("image/screenshot.jpg"),
)
msg = bob.wait_for_incoming_msg()
@@ -1450,35 +1428,3 @@ def test_large_message(acf, rpcdata) -> None:
assert msg.id == msgs_changed_event.msg_id
snapshot = msg.get_snapshot()
assert snapshot.text == "Hello World, this message is bigger than 5 bytes"
def test_is_sending_finished(dc, acf) -> None:
alice, bob = acf.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
bob_chat_alice = bob.create_chat(alice)
assert dc.is_sending_finished()
alice_chat_bob.send_text("Hello!")
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert dc.is_sending_finished()
alice.stop_io()
bob.stop_io()
bob_chat_alice.send_text("Hello back!")
alice_chat_bob.send_text("Hello again!")
assert not dc.is_sending_finished()
alice.start_io()
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert not dc.is_sending_finished()
bob.start_io()
bob.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert dc.is_sending_finished()

View File

@@ -1,132 +0,0 @@
"""Test that docs/schema.sql matches the actual database schema."""
import re
import sqlite3
from pathlib import Path
DOC_PATH = Path(__file__).resolve().parents[2] / "docs" / "schema.sql"
def strip_comments(sql):
return re.sub(r"--[^\n]*", "", sql)
def normalize(stmt):
stmt = re.sub(r"\s+", " ", stmt).strip()
stmt = stmt.replace("CREATE TABLE IF NOT EXISTS ", "CREATE TABLE ")
return re.sub(r'"(\w+)"', r"\1", stmt)
def split_table(body):
items = []
depth = 0
current = ""
for char in body:
if char == "(":
depth += 1
elif char == ")":
depth -= 1
if char == "," and depth == 0:
items.append(current.strip())
current = ""
else:
current += char
if current.strip():
items.append(current.strip())
return items
def parse_schema(sql):
objects = {}
for raw_stmt in strip_comments(sql).split(";"):
stmt = normalize(raw_stmt)
if not stmt:
continue
match = re.match(r"CREATE TABLE (\w+) ?\((.*)\)( STRICT)?$", stmt)
if match:
name, body, strict = match.groups()
objects[f"table {name}"] = {
"items": sorted(split_table(body)),
"strict": bool(strict),
}
continue
match = re.match(r"CREATE (?:UNIQUE )?INDEX (\w+)", stmt)
if match:
objects[f"index {match.group(1)}"] = {"sql": stmt}
continue
objects[stmt[:60]] = {"sql": stmt}
return objects
def format_diff(documented, real):
if "items" in documented and "items" in real:
lines = []
# disregards order
for item in sorted(set(documented["items"]) - set(real["items"])):
lines.append(f" documented but not in the database: {item}")
for item in sorted(set(real["items"]) - set(documented["items"])):
lines.append(f" in the database but not documented: {item}")
if documented["strict"] != real["strict"]:
lines.append(f" STRICT: documented={documented['strict']} actual={real['strict']}")
return "\n".join(lines)
return f" documented: {documented}\n actual: {real}"
def read_database_schema(dbfile):
with sqlite3.connect(f"file:{dbfile}?mode=ro", uri=True) as conn:
rows = conn.execute(
"SELECT sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'",
).fetchall()
return ";\n".join(row[0] for row in rows)
# Tables where every column carries a comment in docs/schema.sql.
# Opt-in: document a table's columns, then add it here to lock it in.
FULLY_DOCUMENTED_TABLES = {
"contacts",
"imap_markseen",
"multi_device_sync",
"transports",
}
def undocumented_columns(sql, tables):
result = []
table = None
documented = False
for line in sql.splitlines():
code, _, comment = line.strip().partition("--")
code = code.strip()
if code.startswith("CREATE TABLE "):
name = code.removeprefix("CREATE TABLE ").partition("(")[0].strip()
table = name if name in tables else None
assert not table or code.endswith("("), f"{code}: want one column per line"
elif code.startswith(")"):
table = None
elif table and code and not re.match(r"(UNIQUE|PRIMARY|FOREIGN|CHECK)\b", code):
column = re.match(r"(\w+) (?!INTEGER PRIMARY KEY)", code)
if column and not documented and not comment:
result.append(f"{table}.{column.group(1)}")
documented = bool(comment) and not code
return result
def test_documented_tables_stay_documented():
missing = undocumented_columns(DOC_PATH.read_text(), FULLY_DOCUMENTED_TABLES)
assert not missing, "columns without a comment in docs/schema.sql:\n" + "\n".join(missing)
def test_documented_schema_matches_database(acf):
account = acf.get_unconfigured_account()
real = parse_schema(read_database_schema(account.get_info()["database_dir"]))
documented = parse_schema(DOC_PATH.read_text())
problems = []
for name in sorted(real.keys() - documented.keys()):
problems.append(f"{name} exists in the database but is not documented")
for name in sorted(documented.keys() - real.keys()):
problems.append(f"{name} is documented but does not exist in the database")
for name in sorted(documented.keys() & real.keys()):
if documented[name] != real[name]:
problems.append(f"{name} differs:\n{format_diff(documented[name], real[name])}")
assert not problems, "documented schema deviates from the database:\n" + "\n".join(problems)

View File

@@ -1,5 +1,5 @@
def test_vcard(acf) -> None:
alice, bob, fiona = acf.get_online_accounts(3)
def test_vcard(acfactory) -> None:
alice, bob, fiona = acfactory.get_online_accounts(3)
bob.create_chat(alice)
alice_contact_bob = alice.create_contact(bob, "Bob")

View File

@@ -1,9 +1,9 @@
def test_webxdc(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
def test_webxdc(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
alice_chat_bob.send_message(text="Let's play chess!", file=data.get_path("webxdc/chess.xdc"))
event = bob.wait_for_incoming_msg_event()
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
@@ -21,7 +21,7 @@ def test_webxdc(acf, rpcdata) -> None:
"isAppSender": False,
"isBroadcast": False,
"sendUpdateInterval": 1000,
"sendUpdateMaxSize": 2**20 * (30 - 1) * 3 // 4,
"sendUpdateMaxSize": 18874368,
}
status_updates = message.get_webxdc_status_updates()
@@ -43,12 +43,12 @@ def test_webxdc(acf, rpcdata) -> None:
]
def test_webxdc_insert_lots_of_updates(acf, rpcdata) -> None:
alice, bob = acf.get_online_accounts(2)
def test_webxdc_insert_lots_of_updates(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
message = alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
message = alice_chat_bob.send_message(text="Let's play chess!", file=data.get_path("webxdc/chess.xdc"))
for i in range(2000):
message.send_webxdc_status_update({"payload": str(i)}, "description")

View File

@@ -1,8 +1,9 @@
[package]
name = "deltachat-rpc-server"
version = "2.61.0-dev"
version = "2.58.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2024"
readme = "README.md"
license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]

View File

@@ -11,7 +11,7 @@ Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`
To install from source run:
```sh
cargo install --locked --git https://github.com/chatmail/core/ deltachat-rpc-server
cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
```
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.61.0-dev"
"version": "2.58.0-dev"
}

View File

@@ -56,7 +56,7 @@ for (const { folder_name, package_name } of platform_package_names) {
if (is_local) {
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
`file:${join(expected_cwd, "/../../deltachat-jsonrpc-bindings/typescript")}`;
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
} else {
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
}

View File

@@ -2,7 +2,7 @@
import { ENV_VAR_NAME } from "./const.js";
const cargoInstallCommand =
"cargo install --locked --git https://github.com/chatmail/core deltachat-rpc-server";
"cargo install --git https://github.com/chatmail/core deltachat-rpc-server";
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
return `deltachat-rpc-server not found:

View File

@@ -8,5 +8,5 @@ license = "MPL-2.0"
proc-macro = true
[dependencies]
syn = "3"
syn = "2"
quote = "1"

View File

@@ -70,7 +70,6 @@ skip = [
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "getrandom", version = "0.2.12" },
{ name = "getrandom", version = "0.3.3" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "hybrid-array", version = "0.2.3" },
@@ -81,7 +80,6 @@ skip = [
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
{ name = "r-efi", version = "5.2.0" },
{ name = "rustix", version = "0.38.44" },
{ name = "rustls-webpki", version = "0.102.8" },
{ name = "serdect", version = "0.2.0" },
@@ -92,7 +90,6 @@ skip = [
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "syn", version = "1.0.109" },
{ name = "syn", version = "2.0.118" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },

View File

@@ -1,827 +0,0 @@
-- Commented SQLite database schema.
--
-- This file should only be used for documentation,
-- do not run this SQL e.g. to create databases.
-- This is because we want to be 100% sure
-- that new users and users who run the migrations
-- get the same database schema.
--
-- This is a dump of the database schema using `sqlite3 dc.db .schema`,
-- formatted, reordered and commented afterwards.
-- Raw dump of the database schema does not have comments
-- for deprecated columns and columns added by migrations.
CREATE TABLE config (
id INTEGER PRIMARY KEY,
keyname TEXT UNIQUE,
value TEXT NOT NULL
);
CREATE INDEX config_index1 ON config (keyname);
CREATE TABLE contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Name of the contact as set by the user.
name TEXT DEFAULT '',
-- Normalized name for search.
name_normalized TEXT,
-- Email address last seen as the From address for this contact.
-- For address-contacts that have empty "fingerprint" this should never change.
-- For key-contacts this address may change when a new signed message is received.
--
-- This is the address messages are sent to unless the key
-- advertises a different set of addresses, in which case
-- this address should be ignored
-- (and not appended to the list of addresses advertised in the key).
addr TEXT DEFAULT '' COLLATE NOCASE,
-- The origin or source of the contact,
-- e.g. whether the contact was added from some chat
-- or via SecureJoin.
origin INTEGER DEFAULT 0,
-- True if the contact is blocked.
-- Unlike the chat, contact is either blocked or not,
-- there is no third "contact request" state.
blocked INTEGER DEFAULT 0,
-- Timestamp of the last time any message was received from this contact.
last_seen INTEGER DEFAULT 0,
-- Key-value parameters.
param TEXT DEFAULT '',
-- Name of the contact as sent by the contact itself.
authname TEXT DEFAULT '',
-- Timestamp of the last time we have sent our avatar to this contact.
-- Used to decide whether to send the avatar.
-- Normally avatars are resent after 14 days.
-- This column is reset to 0 when avatar is changed.
selfavatar_sent INTEGER DEFAULT 0,
-- Last seen message signature from this contact, also known as bio in the UI.
status TEXT DEFAULT '',
-- True if the contact is a bot.
is_bot INTEGER NOT NULL DEFAULT 0,
-- OpenPGP key fingerprint for "key-contacts",
-- empty string for "address-contacts".
fingerprint TEXT NOT NULL DEFAULT '',
-- Unused. Was the ID of the contact that introduced this contact's key.
verifier INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);
CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint);
CREATE TABLE chats (
-- Chat ID 0 should never be used as it is used as a sentinel value in some APIs.
--
-- Chat IDs 1 to 9, including 9, are reserved.
-- The first proper chat gets ID 10, but may not exist if it is deleted.
--
-- Chat ID 3 is the trash chat and this chat ID is assigned to deleted messages
-- to create "tombstones".
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Chat type, e.g. 100 for single chat, 120 for a group etc.
-- Check the Chattype enumeration in the code for concrete values.
type INTEGER DEFAULT 0,
name TEXT DEFAULT '',
-- Normalized name for search.
name_normalized TEXT,
-- 0 for visible chat, 1 for hidden and 2 for contact request.
-- 1 does not necessarily mean that the contact is blocked.
blocked INTEGER DEFAULT 0,
-- Chat-Group-ID header for encrypted groups and channels.
-- Empty for unencrypted groups and single chats.
grpid TEXT DEFAULT '',
-- Key-value parameters.
param TEXT DEFAULT '',
-- Chat visibility.
-- 0 for normal, 1 for archived and 2 for pinned chats.
archived INTEGER DEFAULT 0,
locations_send_begin INTEGER DEFAULT 0,
locations_send_until INTEGER DEFAULT 0,
locations_last_sent INTEGER DEFAULT 0,
-- Time when the chat was created.
-- Used for sorting in the chatlist when the chat has no messages.
created_timestamp INTEGER DEFAULT 0,
-- 0 if the chat is not muted.
-- -1 if the chat is muted forever.
-- Otherwise the timestamp until which the chat is muted.
muted_until INTEGER DEFAULT 0,
-- Disappearing messages timer.
-- 0 means the timer is disabled.
ephemeral_timer INTEGER,
-- Unused. Was 1 for protected chats.
protected INTEGER DEFAULT 0,
gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossip_timestamp table
-- Unused columns, drafts are now tracked as separate
-- messages with a special msgs.state value.
draft_timestamp INTEGER DEFAULT 0,
draft_txt TEXT DEFAULT ''
);
CREATE INDEX chats_index1 ON chats (grpid);
CREATE INDEX chats_index2 ON chats (archived);
CREATE INDEX chats_index3 ON chats (locations_send_until);
CREATE INDEX chats_index4 ON chats (name);
CREATE TABLE chats_descriptions (
chat_id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL DEFAULT ''
) STRICT;
-- Chat member lists.
-- Saved messages has only self.
-- Single chats have only the contact, but not self.
-- Groups have self if we are part of the group.
CREATE TABLE chats_contacts (
chat_id INTEGER,
contact_id INTEGER,
add_timestamp NOT NULL DEFAULT 0,
remove_timestamp NOT NULL DEFAULT 0,
UNIQUE(chat_id, contact_id)
);
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
-- This table contains "message bubbles" that are normally visible
-- and "tombstones" that are put into the trash chat
-- or have a "hidden" column set to the true value.
--
-- Tombstones are used to avoid downloading and processing
-- the same messages twice, e.g. when the message is deleted
-- but another copy of it arrives later.
CREATE TABLE msgs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- The messages may be split into pre- and post-message.
-- Pre-messages have a Chat-Post-Message-ID header
-- which contains the Message-ID of the post-message.
-- Post-messages have a Chat-Is-Post-Message header.
--
-- For outgoing messages that are split into pre-message
-- and post-message, rfc724_mid contains the Message-ID
-- of the post-message, and pre_rfc724_mid
-- contains the Message-ID of the pre-message.
--
-- For incoming messages, when a pre-message arrives,
-- rfc724_mid is set to the Message-ID of the post-message,
-- and pre_rfc724_mid is set to the Message-ID of the arrived pre-messsage.
-- For other messages rfc724_mid is taken from the Message-ID
-- and pre_rfc724_mid is set to empty string.
-- Message-ID as defined in RFC 724 (now replaced by RFC 5322)
rfc724_mid TEXT DEFAULT '',
-- Message-ID of the pre-message.
pre_rfc724_mid TEXT DEFAULT '',
-- Chat ID.
--
-- Chat ID 3 is the trash chat, messages with this ID are tombstones.
chat_id INTEGER DEFAULT 0,
-- Contact ID of the message author.
from_id INTEGER DEFAULT 0,
-- Mostly unused Contact ID of the first recipient or 0.
-- For info messages set to ContactID::INFO (2).
--
-- If to_id is set to 2, the message must be displayed
-- as an info message even if from_id is not set to 2.
--
-- Info messages generated by Webxdc status updates
-- have from_id set to the contact ID of the sender
-- and to_id set to 2.
-- from_id is then used to collapse series of updates
-- into a single message (see test_webxdc_info_msg_cleanup_series),
-- while to_id of 2 makes sure the message is displayed
-- as an info message.
to_id INTEGER DEFAULT 0,
-- Message viewtype.
-- 10 is a text message,
-- 20 is an image etc.
type INTEGER DEFAULT 0,
-- Message state,
-- e.g. 10 for fresh incoming messages,
-- 26 for outgoing delivered message,
-- 28 for outgoing message that got a read receipt.
--
-- Messages sent by self, but arriving from a second device
-- are still considered "outgoing".
state INTEGER DEFAULT 0,
-- Size of the attachment for file parts, otherwise 0.
bytes INTEGER DEFAULT 0,
txt TEXT DEFAULT '', -- Message text for display.
txt_normalized TEXT, -- Message text normalized for search.
txt_raw TEXT DEFAULT '', -- deprecated 2025-03-29
-- Key-value parameters.
param TEXT DEFAULT '',
-- For messages bookmarked into the Saved Messages chat,
-- ID of the original message, making it possible to jump to it.
starred INTEGER DEFAULT 0,
-- True if the message is pinned.
pinned INTEGER NOT NULL DEFAULT 0,
timestamp INTEGER DEFAULT 0, -- Timestamp of the message used for sorting.
timestamp_sent INTEGER DEFAULT 0, -- Timestamp of the message as sent in the Date header.
timestamp_rcvd INTEGER DEFAULT 0,
-- True if the message should not be displayed.
-- Most messages that should not be displayed
-- better go to the trash chat (ID 3),
-- but at least reactions are hidden
-- so they still belong to the chat
-- and can be marked as noticed when the chat is opened.
hidden INTEGER DEFAULT 0,
-- mime_headers column actually contains BLOBs, i.e. it may
-- contain non-UTF8 MIME messages. TEXT was a bad choice, but
-- thanks to SQLite 3 being dynamically typed, there is no need to
-- change column type.
mime_headers TEXT,
-- True if mime_headers column is compressed with Brotli.
mime_compressed INTEGER NOT NULL DEFAULT 0,
mime_modified INTEGER DEFAULT 0,
mime_in_reply_to TEXT,
mime_references TEXT,
-- Location ID for POI locations manually placed on the map.
location_id INTEGER DEFAULT 0,
error TEXT DEFAULT '',
-- Timer value in seconds. For incoming messages this
-- timer starts when message is read, so we want to have
-- the value stored here until the timer starts.
ephemeral_timer INTEGER DEFAULT 0,
-- Timestamp indicating when the message should be
-- deleted. It is convenient to store it here because UI
-- needs this value to display how much time is left until
-- the message is deleted.
ephemeral_timestamp INTEGER DEFAULT 0,
subject TEXT DEFAULT '',
-- Download state for the message.
-- 0 for most messages, meaning the message is fully downloaded.
-- Otherwise 10 for the message available for download etc.
download_state INTEGER DEFAULT 0,
-- Information extracted from Received headers
-- as a plain text for debugging.
hop_info TEXT,
-- True if the message is deleted on the server.
-- If this is true and another copy of the message arrives
-- it should be deleted from the server as well.
deleted INTEGER NOT NULL DEFAULT 0,
--
-- Unused columns.
--
-- Always 1 for new messages.
-- Previously:
-- 0 if the message is a non-chat message,
-- 1 if the message is a chat message,
-- 2 if the message is a non-chat reply to a chat message
msgrmsg INTEGER DEFAULT 1,
server_folder TEXT DEFAULT '', -- Deprecated column that was used before "imap" table, replaced by imap.folder
server_uid INTEGER DEFAULT 0, -- Deprecated column that was used before "imap" table, replaced by imap.uid
-- Unused column formely used to mark the messages
-- that should be moved to the dedicated IMAP folder.
-- It was replaced with imap.target, which is also now
-- used only to mark the messages on IMAP for deletion.
move_state INTEGER DEFAULT 1
);
CREATE INDEX msgs_index1 ON msgs (rfc724_mid);
CREATE INDEX msgs_index2 ON msgs (chat_id);
CREATE INDEX msgs_index3 ON msgs (timestamp);
CREATE INDEX msgs_index4 ON msgs (state);
CREATE INDEX msgs_index5 ON msgs (starred);
CREATE INDEX msgs_index6 ON msgs (location_id);
CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id, timestamp);
CREATE INDEX msgs_index8 ON msgs (ephemeral_timestamp);
CREATE INDEX msgs_index9 ON msgs (pre_rfc724_mid);
CREATE INDEX msgs_index10 ON msgs (pinned) WHERE pinned=1;
CREATE TABLE leftgrps (
id INTEGER PRIMARY KEY,
grpid TEXT DEFAULT ''
);
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
CREATE TABLE msgs_mdns (
msg_id INTEGER,
contact_id INTEGER,
timestamp_sent INTEGER DEFAULT 0
);
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL DEFAULT 0.0,
longitude REAL DEFAULT 0.0,
accuracy REAL DEFAULT 0.0,
timestamp INTEGER DEFAULT 0,
chat_id INTEGER DEFAULT 0,
from_id INTEGER DEFAULT 0,
-- If true, the location is an independent POI
-- and should not be part of the path.
independent INTEGER DEFAULT 0
);
CREATE INDEX locations_index1 ON locations (from_id);
CREATE INDEX locations_index2 ON locations (timestamp);
CREATE TABLE devmsglabels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT,
msg_id INTEGER DEFAULT 0
);
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
-- Table to store QR code tokens for SecureJoin protocol.
CREATE TABLE tokens (
id INTEGER PRIMARY KEY,
-- Namespace is one of:
-- 0 - unknown
-- 100 - invite number
-- 110 - auth
namespc INTEGER NOT NULL,
foreign_key TEXT DEFAULT '' NOT NULL,
token TEXT NOT NULL UNIQUE,
timestamp INTEGER DEFAULT 0 NOT NULL
) STRICT;
-- State of the scanner of the QR code (Bob) in SecureJoin protocol.
CREATE TABLE bobstate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invite TEXT NOT NULL,
next_step INTEGER NOT NULL,
chat_id INTEGER NOT NULL
);
CREATE TABLE smtp2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL, -- Display name to put into the From field.
rfc724_mid TEXT NOT NULL, -- Message-ID
-- Unencrypted payload with some headers.
mime BLOB NOT NULL,
-- True if Autocrypt header should be added before sending.
should_attach_pubkey INTEGER NOT NULL,
-- True if OpenPGP-encrypted message may use compression.
should_compress INTEGER NOT NULL,
-- True if encrypted message should be signed.
should_sign INTEGER NOT NULL,
-- ID of the message in `msgs` table
msg_id INTEGER NOT NULL,
-- Space-separated recipient addresses.
recipients TEXT NOT NULL,
-- Space-separated addresses the message was sent to.
sent_to TEXT NOT NULL DEFAULT '',
-- If true, copy should be sent to self in addition to the recipient list.
--
-- For encrypted messages copy is sent to all addresses.
-- For unencrypted messages, copy is sent to the From address only.
bcc_self INTEGER NOT NULL,
-- True if the message is encrypted.
-- If true, at most one of the shared_secret or encryption_fingerprints should be non-empty.
-- If false, both must be empty.
is_encrypted INTEGER NOT NULL,
-- Shared secret if the message is to be encrypted symmetrically.
shared_secret TEXT NOT NULL DEFAULT '',
-- Space-separated fingerprints of the keys the message should be encrypted to.
encryption_fingerprints TEXT NOT NULL DEFAULT '',
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
) STRICT;
CREATE TABLE smtp_mdns (
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21)
from_id INTEGER NOT NULL, -- id of the contact that sent the message, MDN destination
rfc724_mid TEXT NOT NULL, -- Message-ID header
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send MDN
);
CREATE TABLE smtp_status_updates (
msg_id INTEGER NOT NULL UNIQUE, -- msg_id of the webxdc instance with pending updates
first_serial INTEGER NOT NULL, -- id in msgs_status_updates
last_serial INTEGER NOT NULL, -- id in msgs_status_updates
descr TEXT NOT NULL -- text to send along with the updates
);
-- Table of "sync items" to be grouped into sync messages
-- and sent to own devices.
CREATE TABLE multi_device_sync (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- JSON of the "sync item".
item TEXT DEFAULT ''
);
CREATE TABLE reactions (
msg_id INTEGER NOT NULL, -- id of the message reacted to
contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
PRIMARY KEY(msg_id, contact_id),
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
);
CREATE INDEX reactions_index1 ON reactions (msg_id);
CREATE TABLE pending_reactions (
rfc724_mid TEXT NOT NULL,
contact_id INTEGER NOT NULL,
reaction TEXT NOT NULL,
timestamp INTEGER NOT NULL,
PRIMARY KEY(rfc724_mid, contact_id),
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE
) STRICT;
-- accumulated reactions received from broadcast owner.
-- pairs of reaction and their counts.
-- these pairs are given to the UI (for non-broadcasts, the pairs are calculated from the `reactions` table)
CREATE TABLE broadcasted_reactions (
msg_id INTEGER NOT NULL DEFAULT 0,
reaction TEXT NOT NULL DEFAULT '',
count INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
) STRICT;
CREATE INDEX broadcasted_reactions_index1 ON broadcasted_reactions (msg_id);
-- messages that received reactions from broadcast subscriber to broadcast owner.
-- the broadcast owner will send them, accumulated by chat_id,
-- to all other subscribers every some minutes, and then remove all entries with the chat_id processed.
CREATE TABLE reactions_need_broadcast (
chat_id INTEGER NOT NULL DEFAULT 0,
msg_id INTEGER NOT NULL DEFAULT 0,
UNIQUE (chat_id, msg_id),
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
) STRICT;
CREATE INDEX reactions_need_broadcast_index1 ON reactions_need_broadcast (chat_id);
CREATE TABLE connection_history (
host TEXT NOT NULL, -- server hostname
port INTEGER NOT NULL, -- server port
alpn TEXT NOT NULL, -- ALPN such as smtp or imap
addr TEXT NOT NULL, -- IP address
timestamp INTEGER NOT NULL, -- timestamp of the most recent successful connection
UNIQUE (host, port, alpn, addr)
) STRICT;
CREATE TABLE dns_cache (
hostname TEXT NOT NULL,
address TEXT NOT NULL, -- IPv4 or IPv6 address
timestamp INTEGER NOT NULL,
UNIQUE (hostname, address)
);
CREATE TABLE tls_spki (
host TEXT NOT NULL UNIQUE,
spki_hash TEXT NOT NULL, -- base64 of SPKI SHA-256 hash
timestamp INTEGER NOT NULL -- timestamp of the last time we have seen this key
) STRICT;
CREATE INDEX tls_spki_index_timestamp ON tls_spki (timestamp);
CREATE TABLE http_cache (
url TEXT PRIMARY KEY,
expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
stale INTEGER NOT NULL, -- When the cache entry is considered stale, timestamp in seconds.
blobname TEXT NOT NULL,
mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
) STRICT;
-- Webxdc updates.
CREATE TABLE msgs_status_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
msg_id INTEGER,
update_item TEXT DEFAULT '',
uid TEXT UNIQUE,
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE
);
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
-- Webxdc realtime.
CREATE TABLE iroh_gossip_peers (
msg_id INTEGER not NULL,
topic BLOB NOT NULL,
public_key BLOB NOT NULL,
relay_server TEXT, UNIQUE (topic, public_key),
PRIMARY KEY(topic, public_key)
) STRICT;
--
-- OpenPGP.
--
-- Storage for own private keys.
-- Only one of the keys is used.
--
-- In the past mulitple keys could be imported,
-- so this table can contain multiple keys for existing users.
-- Used key is identified by the "key_id" config value.
-- Other keys must never be used, even for decryption.
CREATE TABLE keypairs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- OpenPGP private key stored as a binary blob.
private_key UNIQUE NOT NULL,
-- Unused public key aka OpenPGP certificate.
-- Stored only for compatibility.
-- OpenPGP certificate is generated at runtime from the private key.
public_key UNIQUE NOT NULL,
--
-- Unused columns.
--
--
-- Columns "addr", "is_default" and "created" were
-- dropped in migration 107
-- but added back for compatibility in migration 110.
addr TEXT DEFAULT '' COLLATE NOCASE,
-- Migrated into "key_id" config value in migration 107.
is_default INTEGER DEFAULT 0,
created INTEGER DEFAULT 0
);
CREATE TABLE public_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key.
public_key BLOB NOT NULL -- Binary key, not ASCII-armored
) STRICT;
CREATE INDEX public_key_index ON public_keys (fingerprint);
CREATE TABLE gossip_timestamp (
chat_id INTEGER NOT NULL,
fingerprint TEXT NOT NULL, -- Upper-case fingerprint of the key.
timestamp INTEGER NOT NULL,
UNIQUE (chat_id, fingerprint)
) STRICT;
CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
-- Timestamps of distributing own key in the Autocrypt header of MDNs.
CREATE TABLE mdn_autocrypt_timestamp (
fingerprint TEXT PRIMARY KEY NOT NULL, -- Upper-case fingerprint of the recipient key.
attached_timestamp INTEGER NOT NULL
) STRICT;
-- Passwords used to derive symmetric keys for broadcast lists aka channels.
CREATE TABLE broadcast_secrets(
chat_id INTEGER PRIMARY KEY NOT NULL,
secret TEXT NOT NULL
) STRICT;
-- Candidate chatmail relays for automatic relay management.
CREATE TABLE relay_candidates(
host TEXT PRIMARY KEY NOT NULL,
last_tried INTEGER NOT NULL DEFAULT 0 -- Timestamp of the last connection attempt.
) STRICT;
CREATE TABLE transports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Email address associated with this transport.
-- It uniquely identifies the transport.
addr TEXT NOT NULL,
-- JSON with the settings entered by the user.
-- The settings are not necessary entered manually,
-- but can be entered by scanning the QR code.
-- These settings are only used during the transport configuration process.
entered_param TEXT NOT NULL,
-- JSON with the settings used to connect to the transport.
-- These settings are derived from entered parameters
-- and possibly autoconfiguration XML fetched over HTTPS.
-- The settings stored here are known to have worked at least once,
-- this is ensured during configuration.
configured_param TEXT NOT NULL,
-- Bumping this forces re-signing the key:
-- the relay-list signature is created at MAX(add_timestamp, remove_timestamp)
-- over this table and `removed_transports`, and contacts keep the newest one.
add_timestamp INTEGER NOT NULL DEFAULT 0,
-- Unused since migration 165, which removed unpublished transports.
is_published INTEGER DEFAULT 1 NOT NULL,
-- Time when the transport was last used to receive a message.
last_rcvd_timestamp INTEGER NOT NULL DEFAULT 0,
UNIQUE(addr)
);
CREATE TABLE removed_transports (
addr TEXT NOT NULL,
remove_timestamp INTEGER NOT NULL,
UNIQUE(addr)
) STRICT;
CREATE TABLE imap (
id INTEGER PRIMARY KEY AUTOINCREMENT,
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
rfc724_mid TEXT NOT NULL, -- Message-ID header
folder TEXT NOT NULL, -- IMAP folder
target TEXT NOT NULL, -- Destination folder. Empty string means that the message shall be deleted.
uid INTEGER NOT NULL, -- UID
uidvalidity INTEGER NOT NULL,
UNIQUE (transport_id, folder, uid, uidvalidity)
) STRICT;
CREATE INDEX imap_folder ON imap(transport_id, folder);
CREATE INDEX imap_rfc724_mid ON imap(transport_id, rfc724_mid);
CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid);
CREATE TABLE imap_markseen (
id INTEGER PRIMARY KEY NOT NULL,
FOREIGN KEY(id) REFERENCES imap(id) ON DELETE CASCADE
);
CREATE TABLE imap_sync (
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
folder TEXT NOT NULL,
uidvalidity INTEGER NOT NULL DEFAULT 0,
uid_next INTEGER NOT NULL DEFAULT 0,
modseq INTEGER NOT NULL DEFAULT 0,
UNIQUE (transport_id, folder)
) STRICT;
CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
CREATE TABLE download (
rfc724_mid TEXT PRIMARY KEY,
msg_id INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE available_post_msgs (
rfc724_mid TEXT PRIMARY KEY
) STRICT;
--
-- Statistics.
--
CREATE TABLE stats_securejoin_sources(
source INTEGER PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE stats_securejoin_uipaths(
uipath INTEGER PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE stats_securejoin_invites(
already_existed INTEGER NOT NULL,
already_verified INTEGER NOT NULL, -- unused, always 0
type TEXT NOT NULL
) STRICT;
CREATE TABLE stats_msgs(
chattype INTEGER PRIMARY KEY,
verified INTEGER NOT NULL DEFAULT 0, -- unused, always 0
unverified_encrypted INTEGER NOT NULL DEFAULT 0, -- counts all encrypted messages
unencrypted INTEGER NOT NULL DEFAULT 0,
only_to_self INTEGER NOT NULL DEFAULT 0,
last_counted_msg_id INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE stats_sending_enabled_events(timestamp INTEGER NOT NULL) STRICT;
CREATE TABLE stats_sending_disabled_events(timestamp INTEGER NOT NULL) STRICT;
-- Deprecated and unused tables.
--
-- We don't immediately drop unused tables,
-- because we want users to be able to downgrade.
-- Deprecated table for Autocrypt peer states.
-- It is replaced by the new "public_keys" table in migration 132
-- that introduced "key contacts" which have a fixed fingerprint.
CREATE TABLE acpeerstates (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
last_seen INTEGER DEFAULT 0,
last_seen_autocrypt INTEGER DEFAULT 0,
public_key,
prefer_encrypted INTEGER DEFAULT 0,
gossip_timestamp INTEGER DEFAULT 0,
gossip_key,
public_key_fingerprint TEXT DEFAULT '',
gossip_key_fingerprint TEXT DEFAULT '',
verified_key,
verified_key_fingerprint TEXT DEFAULT '',
verifier TEXT DEFAULT '',
secondary_verified_key,
secondary_verified_key_fingerprint TEXT DEFAULT '',
secondary_verifier TEXT DEFAULT '',
backward_verified_key_id -- What we think the contact has as our verified key
INTEGER,
UNIQUE (addr) -- Only one peerstate per address
);
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
-- Deprecated table previously used to upload sync messages using IMAP APPEND.
-- Sync messages are sent over SMTP now as they should be sent to multiple transports.
CREATE TABLE imap_send (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mime TEXT NOT NULL, -- Message content
msg_id INTEGER NOT NULL, -- ID of the message in the `msgs` table
attempts INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
);
-- Deprecated table used for a job system.
CREATE TABLE jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
added_timestamp INTEGER,
desired_timestamp INTEGER DEFAULT 0,
action INTEGER,
foreign_id INTEGER,
param TEXT DEFAULT '',
thread INTEGER DEFAULT 0,
tries INTEGER DEFAULT 0
);
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
-- Backup of the keypairs left after migration 107.
-- Not used by any code, it was only for a recovery
-- in case migration 107 goes wrong.
CREATE TABLE old_keypairs (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
is_default INTEGER DEFAULT 0,
private_key,
public_key,
created INTEGER DEFAULT 0
);
-- Unused table, previously introduced for AEAP mechanism
-- which is replaced by key contacts.
CREATE TABLE sending_domains(
domain TEXT PRIMARY KEY,
dkim_works INTEGER DEFAULT 0
);
-- Replaced with smtp2.
CREATE TABLE smtp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rfc724_mid TEXT NOT NULL, -- Message-ID
mime TEXT NOT NULL, -- SMTP payload
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
recipients TEXT NOT NULL, -- List of recipients separated by space
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
);

445
flake.nix
View File

@@ -29,7 +29,11 @@
rustc = fenixToolchain;
};
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
version = manifest.version;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
});
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
rustSrc = nix-filter.lib {
root = ./.;
@@ -47,7 +51,6 @@
./deltachat-contact-tools
./deltachat-ffi
./deltachat-jsonrpc
./deltachat-jsonrpc-bindings
./deltachat-ratelimit
./deltachat-repl
./deltachat-rpc-client
@@ -80,7 +83,7 @@
naersk'.buildPackage {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
inherit version;
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
@@ -88,22 +91,221 @@
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
};
mkWin64RustPackage = packageName:
let
pkgsWin64 = pkgs.pkgsCross.mingwW64;
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naerskWin = pkgs.callPackage naersk {
cargo = toolchainWin;
rustc = toolchainWin;
};
in
naerskWin.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
depsBuildBuild = [
pkgsWin64.stdenv.cc
];
buildInputs = [
pkgsWin64.windows.pthreads
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
mkWin64RustPackage = pkgs.callPackage ./nix/win64-package.nix {
inherit naersk system fenixPkgs version;
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
"-L"
"native=${pkgsWin64.windows.pthreads}/lib"
];
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
};
mkWin32RustPackage = packageName:
let
pkgsWin32 = pkgs.pkgsCross.mingw32;
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naerskWin = pkgs.callPackage naersk {
cargo = toolchainWin;
rustc = toolchainWin;
};
# Get rid of MCF Gthread library.
# See <https://github.com/NixOS/nixpkgs/issues/156343>
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
# for details.
#
# Use DWARF-2 instead of SJLJ for exception handling.
winCC = pkgsWin32.buildPackages.wrapCC (
(pkgsWin32.buildPackages.gcc-unwrapped.override
({
threadsCross = {
model = "win32";
package = null;
};
})).overrideAttrs (oldAttr: {
configureFlags = oldAttr.configureFlags ++ [
"--disable-sjlj-exceptions"
"--with-dwarf2"
];
})
);
in
naerskWin.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.nasm # aws-lc-sys requires it
];
depsBuildBuild = [
winCC
];
buildInputs = [
pkgsWin32.windows.pthreads
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
"-L"
"native=${pkgsWin32.windows.pthreads}/lib"
];
CC = "${winCC}/bin/${winCC.targetPrefix}cc";
LD = "${winCC}/bin/${winCC.targetPrefix}cc";
};
mkCrossRustPackage = arch: packageName:
let
crossTarget = arch2targets."${arch}";
pkgsCross = import nixpkgs {
system = system;
crossSystem.config = crossTarget;
};
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
toolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
in
naersk-lib.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
};
androidAttrs = {
armeabi-v7a = {
cc = "armv7a-linux-androideabi21-clang";
rustTarget = "armv7-linux-androideabi";
};
arm64-v8a = {
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
x86 = {
cc = "i686-linux-android21-clang";
rustTarget = "i686-linux-android";
};
x86_64 = {
cc = "x86_64-linux-android21-clang";
rustTarget = "x86_64-linux-android";
};
};
mkWin32RustPackage = pkgs.callPackage ./nix/win32-package.nix {
inherit naersk system fenixPkgs version;
};
mkAndroidRustPackage = arch: packageName:
let
rustTarget = androidAttrs.${arch}.rustTarget;
toolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
targetCcName = androidAttrs.${arch}.cc;
targetCc = "${targetToolchain}/bin/${targetCcName}";
in
naersk-lib.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
mkCrossRustPackage = pkgs.callPackage ./nix/cross-rust-package.nix {
inherit nixpkgs arch2targets naersk fenixPkgs system rustSrc version;
};
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${targetCc}";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
mkAndroidRustPackage = pkgs.callPackage ./nix/android-package.nix {
inherit naersk fenixPkgs system rustSrc android version;
};
CC = "${targetCc}";
LD = "${targetCc}";
};
mkAndroidPackages = arch:
let
@@ -113,7 +315,32 @@
"deltachat-rpc-server-${arch}-android" = rpc-server;
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
"deltachat-rpc-server-${arch}-android-wheel" =
mkWheel { inherit rpc-server; arch = "${arch}-android"; };
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-android-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch}-android tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
};
mkRustPackages = arch:
@@ -123,10 +350,34 @@
{
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
"deltachat-rpc-server-${arch}" = rpc-server;
"deltachat-rpc-server-${arch}-wheel" = mkWheel { inherit rpc-server; arch = "${arch}"; };
"deltachat-rpc-server-${arch}-wheel" =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
};
mkWheel = pkgs.callPackage ./nix/wheel.nix { inherit nix-filter version; root = ./.; };
in
{
formatter = pkgs.nixpkgs-fmt;
@@ -150,29 +401,116 @@
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win64-wheel =
mkWheel { rpc-server = deltachat-rpc-server-win64; arch = "win64"; binaryName = "deltachat-rpc-server.exe"; };
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win64-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win64
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win32-wheel =
mkWheel
{ rpc-server = deltachat-rpc-server-win32; arch = "win32"; binaryName = "deltachat-rpc-server.exe"; };
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win32-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win32
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
# Run `nix build .#docs` to get C docs generated in `./result/`.
docs = pkgs.callPackage ./nix/c-docs.nix { inherit version; };
docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
libdeltachat = pkgs.callPackage ./nix/libdeltachat.nix {
inherit fenixToolchain rustSrc cargoLock fenixPkgs version;
};
libdeltachat =
let
rustPlatform = (pkgs.makeRustPlatform {
cargo = fenixToolchain;
rustc = fenixToolchain;
});
in
pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
deltachat-rpc-client = pkgs.callPackage ./nix/deltachat-rpc-client.nix {
inherit version;
};
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
rustPlatform.cargoSetupHook
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
deltachat-rpc-client =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
};
deltachat-python =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-python";
inherit version;
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
@@ -190,12 +528,51 @@
pkgs.python3Packages.requests
];
};
python-docs = pkgs.callPackage ./nix/python-docs.nix {
inherit deltachat-python deltachat-rpc-client version;
};
python-docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx-rtd-theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
};
devShells.default = import ./nix/shell.nix { inherit nixpkgs fenix system; };
devShells.default =
let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in
pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
(python3.withPackages (pypkgs: with pypkgs; [
tox
]))
nodejs
];
};
}
);
}

View File

@@ -1,63 +0,0 @@
{ pkgs, naersk, fenixPkgs, system, version, rustSrc, android }:
arch: packageName:
let
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
});
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
androidAttrs = {
armeabi-v7a = {
cc = "armv7a-linux-androideabi21-clang";
rustTarget = "armv7-linux-androideabi";
};
arm64-v8a = {
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
x86 = {
cc = "i686-linux-android21-clang";
rustTarget = "i686-linux-android";
};
x86_64 = {
cc = "x86_64-linux-android21-clang";
rustTarget = "x86_64-linux-android";
};
};
rustTarget = androidAttrs.${arch}.rustTarget;
toolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
targetCcName = androidAttrs.${arch}.cc;
targetCc = "${targetToolchain}/bin/${targetCcName}";
in
naersk-lib.buildPackage {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
inherit version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${targetCc}";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${targetCc}"
];
CC = "${targetCc}";
LD = "${targetCc}";
}

View File

@@ -1,9 +0,0 @@
{ pkgs, version }:
pkgs.stdenv.mkDerivation {
pname = "docs";
inherit version;
src = pkgs.lib.cleanSource ../.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
}

View File

@@ -1,45 +0,0 @@
{ pkgs, nixpkgs, arch2targets, naersk, fenixPkgs, system, rustSrc, version }:
arch: packageName:
let
crossTarget = arch2targets."${arch}";
pkgsCross = import nixpkgs {
system = system;
crossSystem.config = crossTarget;
};
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
toolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
targetCc = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
in
naersk-lib.buildPackage {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
inherit version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${targetCc}";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${targetCc}"
];
CC = "${targetCc}";
LD = "${targetCc}";
}

View File

@@ -1,11 +0,0 @@
{ pkgs, version }:
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client";
inherit version;
src = pkgs.lib.cleanSource ../deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
}

View File

@@ -1,26 +0,0 @@
{ pkgs, fenixToolchain, rustSrc, cargoLock, fenixPkgs, version }:
let
rustPlatform = (pkgs.makeRustPlatform {
cargo = fenixToolchain;
rustc = fenixToolchain;
});
in
pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
inherit version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
rustPlatform.cargoSetupHook
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
}

View File

@@ -1,15 +0,0 @@
{ pkgs, version, deltachat-python, deltachat-rpc-client }:
pkgs.stdenv.mkDerivation {
pname = "docs";
inherit version;
src = pkgs.lib.cleanSource ../.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx-rtd-theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
}

View File

@@ -1,27 +0,0 @@
{ nixpkgs, fenix, system }:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ fenix.overlays.default ];
};
in
pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
(python3.withPackages (pypkgs: [
pypkgs.tox
]))
nodejs
];
}

View File

@@ -1,28 +0,0 @@
{ pkgs, nix-filter, version, root }:
{ rpc-server, arch, binaryName ? "deltachat-rpc-server" }:
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
inherit version;
src = nix-filter.lib {
inherit root;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/${binaryName} tmp/${binaryName}
python3 scripts/wheel-rpc-server.py ${arch} tmp/${binaryName}
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
}

View File

@@ -1,69 +0,0 @@
{ pkgs, naersk, fenixPkgs, system, version }:
packageName:
let
pkgsWin32 = pkgs.pkgsCross.mingw32;
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naerskWin = pkgs.callPackage naersk {
cargo = toolchainWin;
rustc = toolchainWin;
};
# Get rid of MCF Gthread library.
# See <https://github.com/NixOS/nixpkgs/issues/156343>
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
# for details.
#
# Use DWARF-2 instead of SJLJ for exception handling.
winCC = pkgsWin32.buildPackages.wrapCC (
(pkgsWin32.buildPackages.gcc-unwrapped.override
({
threadsCross = {
model = "win32";
package = null;
};
})).overrideAttrs (oldAttr: {
configureFlags = oldAttr.configureFlags ++ [
"--disable-sjlj-exceptions"
"--with-dwarf2"
];
})
);
targetCc = "${winCC}/bin/${winCC.targetPrefix}cc";
in
naerskWin.buildPackage {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
inherit version;
strictDeps = true;
src = pkgs.lib.cleanSource ../.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.nasm # aws-lc-sys requires it
];
depsBuildBuild = [
winCC
];
buildInputs = [
pkgsWin32.windows.pthreads
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${targetCc}";
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${targetCc}"
"-L"
"native=${pkgsWin32.windows.pthreads}/lib"
];
CC = "${targetCc}";
LD = "${targetCc}";
}

View File

@@ -1,47 +0,0 @@
{ pkgs, naersk, fenixPkgs, system, version }:
packageName:
let
pkgsWin64 = pkgs.pkgsCross.mingwW64;
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naerskWin = pkgs.callPackage naersk {
cargo = toolchainWin;
rustc = toolchainWin;
};
targetCc = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
in
naerskWin.buildPackage {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
inherit version;
strictDeps = true;
src = pkgs.lib.cleanSource ../.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
depsBuildBuild = [
pkgsWin64.stdenv.cc
];
buildInputs = [
pkgsWin64.windows.pthreads
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${targetCc}";
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${targetCc}"
"-L"
"native=${pkgsWin64.windows.pthreads}/lib"
];
CC = "${targetCc}";
LD = "${targetCc}";
}

View File

@@ -17,7 +17,7 @@ Install ``deltachat-rpc-server``
To get ``deltachat-rpc-server`` binary you have three options:
1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``.
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --locked --git https://github.com/chatmail/core/ deltachat-rpc-server``.
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server``.
3. Download prebuilt release from https://github.com/chatmail/core/releases and install it into ``PATH``.
Check that ``deltachat-rpc-server`` is installed and can run::

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.61.0-dev"
version = "2.58.0-dev"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"

View File

@@ -259,15 +259,6 @@ class Chat:
# ------ chat messaging API ------------------------------
def _reload_sent_msg(self, msg: Message, sent_id: int) -> Message:
"""Helper to reload just sent message"""
sent_msg = Message.from_db(self.account, sent_id)
if sent_msg is None:
raise ValueError("cannot load just sent message from the database")
# modify message in place to avoid bad state for the caller
msg._dc_msg = sent_msg._dc_msg
return msg
def send_msg(self, msg: Message) -> Message:
"""send a message by using a ready Message object.
@@ -283,25 +274,12 @@ class Chat:
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
return self._reload_sent_msg(msg, sent_id)
def send_msg_sync(self, msg: Message) -> Message:
"""Send a message synchronously.
This bypasses the IO scheduler and creates its own SMTP connection.
:param msg: a :class:`deltachat.message.Message` instance
previously returned by
e.g. :meth:`deltachat.message.Message.new_empty`.
:raises ValueError: if message can not be sent.
:returns: a :class:`deltachat.message.Message` instance as
sent out. This is the same object as was passed in, which
has been modified with the new state of the core.
"""
sent_id = lib.dc_send_msg_sync(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
return self._reload_sent_msg(msg, sent_id)
# modify message in place to avoid bad state for the caller
sent_msg = Message.from_db(self.account, sent_id)
if sent_msg is None:
raise ValueError("cannot load just sent message from the database")
msg._dc_msg = sent_msg._dc_msg
return msg
def send_text(self, text):
"""send a text message and return the resulting Message instance.

View File

@@ -71,6 +71,17 @@ class Contact:
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False)
def is_verified(self) -> bool:
"""Return True if the contact is verified."""
return lib.dc_contact_is_verified(self._dc_contact) == 2
def get_verifier(self, contact) -> Optional["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)
def get_profile_image(self) -> Optional[str]:
"""Get contact profile image.

View File

@@ -7,7 +7,6 @@ import imaplib
import io
import pathlib
import ssl
import time
from contextlib import contextmanager
from typing import List, TYPE_CHECKING
@@ -43,22 +42,10 @@ class DirectImap:
host = user.rsplit("@")[-1]
pw = self.account.get_config("mail_pw")
ssl_context = ssl.create_default_context()
if host.startswith("_"):
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
self.conn.login(user, pw)
while True:
try:
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")
return
except (OSError, imaplib.IMAP4.abort) as e:
# OSError covers ssl.SSLError, "abort" is a dropped connection;
# permanent IMAP errors (login rejected etc) must not be retried.
print(f"direct_imap connect to {host} failed ({e!r}), retrying")
time.sleep(1)
self.select_folder("INBOX")
def shutdown(self):
try:
@@ -104,11 +91,11 @@ class DirectImap:
def get_all_messages(self) -> List[MailMessage]:
assert not self._idling
return list(self.conn.fetch(mark_seen=False))
return list(self.conn.fetch())
def get_unread_messages(self) -> List[str]:
assert not self._idling
return [msg.uid for msg in self.conn.fetch(AND(seen=False), mark_seen=False)]
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
def mark_all_read(self):
messages = self.get_unread_messages()
@@ -183,7 +170,7 @@ class DirectImap:
self.conn.append(bytes(msg, encoding="ascii"), folder)
def get_uid_by_message_id(self, message_id) -> str:
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)), mark_seen=False)]
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]
@@ -193,6 +180,9 @@ class IdleManager:
def __init__(self, direct_imap) -> None:
self.direct_imap = direct_imap
self.log = direct_imap.account.log
# fetch latest messages before starting idle so that it only
# returns messages that arrive anew
self.direct_imap.conn.fetch("1:*")
self.direct_imap.conn.idle.start()
def check(self, timeout=None) -> List[bytes]:

View File

@@ -226,10 +226,6 @@ class EventThread(threading.Thread):
def __init__(self, account: Account) -> None:
self.account = account
self.event_emitter = ffi.gc(
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
)
super(EventThread, self).__init__(name="events")
self.daemon = True
self._marked_for_shutdown = False
@@ -254,9 +250,13 @@ class EventThread(threading.Thread):
def run(self) -> None:
"""get and run events until shutdown."""
with self.log_execution("EVENT THREAD"):
event_emitter = ffi.gc(
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
)
while not self._marked_for_shutdown:
with self.swallow_and_log_exception("Unexpected error in event thread"):
event = lib.dc_get_next_event(self.event_emitter)
event = lib.dc_get_next_event(event_emitter)
if event == ffi.NULL or self._marked_for_shutdown:
break
self._process_event(event)

View File

@@ -1,6 +1,7 @@
import fnmatch
import io
import os
import pathlib
import queue
import subprocess
import sys
@@ -9,10 +10,7 @@ import time
import weakref
import random
from queue import Queue
from typing import Callable, Dict, List, Optional
from .capi import lib
from .cutil import as_dc_charpointer
from typing import Callable, Dict, List, Optional, Set
import pytest
from _pytest._code import Source
@@ -140,6 +138,94 @@ def pytest_report_header(config):
return summary
@pytest.fixture(scope="session")
def testprocess(request):
"""Return live account configuration manager.
The returned object is a :class:`TestProcess` object."""
return TestProcess(pytestconfig=request.config)
class TestProcess:
"""A pytest session-scoped instance to help with managing "live" account configurations."""
_addr2files: Dict[str, Dict[pathlib.Path, bytes]]
def __init__(self, pytestconfig) -> None:
self.pytestconfig = pytestconfig
self._addr2files = {}
self._configlist: List[Dict[str, str]] = []
def get_liveconfig_producer(self):
"""provide live account configs, cached on a per-test-process scope
so that test functions can reuse already known live configs.
"""
chatmail_opt = self.pytestconfig.getoption("--chatmail")
if chatmail_opt:
# Use a chatmail instance.
domain = chatmail_opt
MAX_LIVE_CREATED_ACCOUNTS = 10
for index in range(MAX_LIVE_CREATED_ACCOUNTS):
try:
yield self._configlist[index]
except IndexError:
part = "".join(random.choices("2345789acdefghjkmnpqrstuvwxyz", k=6))
username = f"ci-{part}"
password = f"{username}${username}"
addr = f"{username}@{domain}"
config = {"addr": addr, "mail_pw": password}
print("newtmpuser {}: addr={}".format(index, config["addr"]))
self._configlist.append(config)
yield config
pytest.fail(f"more than {MAX_LIVE_CREATED_ACCOUNTS} live accounts requested.")
else:
pytest.skip(
"specify CHATMAIL_DOMAIN or --chatmail to provide live accounts",
)
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
db_target_path = pathlib.Path(db_target_path)
assert not db_target_path.exists()
try:
filescache = self._addr2files[cache_addr]
except KeyError:
print("CACHE FAIL for", cache_addr)
return False
else:
print("CACHE HIT for", cache_addr)
targetdir = db_target_path.parent
write_dict_to_dir(filescache, targetdir)
return True
def cache_maybe_store_configured_db_files(self, acc):
addr = acc.get_config("addr")
assert acc.is_configured()
# don't overwrite existing entries
if addr not in self._addr2files:
print("storing cache for", addr)
basedir = pathlib.Path(acc.get_blobdir()).parent
self._addr2files[addr] = create_dict_from_files_in_path(basedir)
return True
def create_dict_from_files_in_path(base):
cachedict = {}
for path in base.glob("**/*"):
if path.is_file():
cachedict[path.relative_to(base)] = path.read_bytes()
return cachedict
def write_dict_to_dir(dic, target_dir):
assert dic
for relpath, content in dic.items():
path = target_dir.joinpath(relpath)
if not path.parent.exists():
os.makedirs(path.parent)
path.write_bytes(content)
@pytest.fixture()
def data(request):
"""Test data."""
@@ -192,15 +278,23 @@ class ACSetup:
_configured_events: Queue
def __init__(self, init_time) -> None:
def __init__(self, testprocess, init_time) -> None:
self._configured_events = Queue()
self._account2state: Dict[Account, str] = {}
self._account2config: Dict[Account, Dict[str, str]] = {}
self._imap_cleaned: Set[str] = set()
self.testprocess = testprocess
self.init_time = init_time
def log(self, *args):
print("[acsetup]", f"{time.time() - self.init_time:.3f}", *args)
def add_configured(self, account):
"""add an already configured account."""
assert account.is_configured()
self._account2state[account] = self.CONFIGURED
self.log("added already configured account", account, account.get_config("addr"))
def start_configure(self, account):
"""add an account and start its configure process."""
@@ -232,7 +326,7 @@ class ACSetup:
for each account which either is CONFIGURED already or which is CONFIGURING
and successfully completing the configuration process.
"""
print("bring_online finds accounts=", self._account2state)
print("wait_all_configured finds accounts=", self._account2state)
for acc, state in self._account2state.items():
if state == self.CONFIGURED:
self._onconfigure_start_io(acc)
@@ -268,12 +362,23 @@ class ACSetup:
acc.add_account_plugin(logger, name="logger-" + acc._logid)
def init_imap(self, acc):
"""initialize direct_imap for the account."""
"""initialize direct_imap and cleanup server state."""
from deltachat.direct_imap import DirectImap
assert acc.is_configured()
if not hasattr(acc, "direct_imap"):
acc.direct_imap = DirectImap(acc)
addr = acc.get_config("addr")
if addr not in self._imap_cleaned:
imap = acc.direct_imap
for folder in imap.list_folders():
if folder.lower() == "inbox" or folder.lower() == "deltachat":
assert imap.select_folder(folder)
imap.delete("1:*", expunge=True)
else:
imap.conn.folder.delete(folder)
acc.log(f"imap cleaned for addr {addr}")
self._imap_cleaned.add(addr)
class ACFactory:
@@ -285,19 +390,24 @@ class ACFactory:
_acsetup: ACSetup
_preconfigured_keys: List[str]
def __init__(self, request, tmpdir, data) -> None:
def __init__(self, request, testprocess, tmpdir, data) -> None:
self.init_time = time.time()
self.tmpdir = tmpdir
self.pytestconfig = request.config
self.data = data
self.testprocess = testprocess
self._liveconfig_producer = testprocess.get_liveconfig_producer()
self._finalizers = []
self._accounts = []
self._acsetup = ACSetup(self.init_time)
self._acsetup = ACSetup(testprocess, self.init_time)
self._preconfigured_keys = ["alice", "bob", "charlie", "dom", "elena", "fiona"]
self.set_logging_default(False)
request.addfinalizer(self.finalize)
def log(self, *args):
print("[acfactory]", f"{time.time() - self.init_time:.3f}", *args)
def finalize(self):
while self._finalizers:
fin = self._finalizers.pop()
@@ -314,27 +424,33 @@ class ACFactory:
acc.disable_logging()
def get_next_liveconfig(self):
"""Return a fresh account configuration with unique address and password."""
chatmail_domain = self.pytestconfig.getoption("--chatmail")
if not chatmail_domain:
pytest.skip(
"specify CHATMAIL_DOMAIN or --chatmail to provide live accounts",
)
part = "".join(random.choices("2345789acdefghjkmnpqrstuvwxyz", k=6))
username = f"ci-{part}"
password = f"{username}${username}"
addr = f"{username}@{chatmail_domain}"
configdict = {"addr": addr, "mail_pw": password}
print(f"newtmpuser: addr={addr}")
"""
Base function to get functional online configurations
where we can make valid SMTP and IMAP connections with.
"""
configdict = next(self._liveconfig_producer).copy()
if self.pytestconfig.getoption("--strict-tls"):
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
assert "addr" in configdict and "mail_pw" in configdict
return configdict
def _get_cached_account(self, addr) -> Optional[Account]:
if addr in self.testprocess._addr2files:
return self._getaccount(addr)
return None
def get_unconfigured_account(self, closed=False) -> Account:
return self._getaccount(closed=closed)
def _getaccount(self, try_cache_addr=None, closed=False) -> Account:
logid = f"ac{len(self._accounts) + 1}"
# we need to use fixed database basename for maybe_cache_* functions to work
path = self.tmpdir.mkdir(logid).join("dc.db")
if try_cache_addr:
self.testprocess.cache_maybe_retrieve_configured_db_files(try_cache_addr, path)
ac = Account(path.strpath, logging=self._logging, closed=closed)
ac._logid = logid # later instantiated FFIEventLogger needs this
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
@@ -369,7 +485,6 @@ class ACFactory:
ac.open(passphrase)
acname = ac._logid
addr = f"{acname}@offline.org"
lib.dc_add_pseudo_transport(ac._dc_context, as_dc_charpointer(addr))
ac.update_config(
{
"configured_addr": addr,
@@ -378,19 +493,25 @@ class ACFactory:
)
self._preconfigure_key(ac)
self._acsetup.init_logging(ac)
assert ac.is_configured(), "Pseudo configured account should look like if it is configured"
return ac
def new_online_configuring_account(self, cloned_from=None, **kwargs) -> Account:
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs) -> Account:
if cloned_from is None:
configdict = self.get_next_liveconfig()
else:
# XXX we might want to transfer the key to the new account
configdict = {
"addr": cloned_from.get_config("addr"),
"mail_pw": cloned_from.get_config("mail_pw"),
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
}
configdict.update(kwargs)
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
if ac is not None:
# make sure we consume a preconfig key, as if we had created a fresh account
self._preconfigured_keys.pop(0)
self._acsetup.add_configured(ac)
return ac
ac = self.prepare_account_from_liveconfig(configdict)
self._acsetup.start_configure(ac)
return ac
@@ -415,8 +536,11 @@ class ACFactory:
print("all accounts online")
def get_online_accounts(self, num):
accounts = [self.new_online_configuring_account() for i in range(num)]
accounts = [self.new_online_configuring_account(cache=True) for i in range(num)]
self.bring_accounts_online()
# we cache fully configured and started accounts
for acc in accounts:
self.testprocess.cache_maybe_store_configured_db_files(acc)
return accounts
def run_bot_process(self, module, ffi=True):
@@ -491,15 +615,16 @@ class ACFactory:
@pytest.fixture()
def acfactory(request, tmpdir, data):
def acfactory(request, tmpdir, testprocess, data):
"""Account factory."""
am = ACFactory(request=request, tmpdir=tmpdir, data=data)
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
yield am
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
if request.config.getoption("--extra-info"):
if testprocess.pytestconfig.getoption("--extra-info"):
logfile = io.StringIO()
am.dump_imap_summary(logfile=logfile)
print(logfile.getvalue())
# request.node.add_report_section("call", "imap-server-state", s)
class BotProcess:

View File

@@ -1,5 +1,7 @@
import time
import deltachat as dc
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
@@ -61,9 +63,9 @@ class TestGroupStressTests:
assert msg.is_encrypted()
def test_qr_group_join_and_chatting(acfactory, lp):
def test_qr_verified_group_and_chatting(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
lp.sec("ac1: create group QR, ac2 scans and joins")
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello")
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
@@ -84,11 +86,15 @@ def test_qr_group_join_and_chatting(acfactory, lp):
msg_out = chat1.send_text("hello")
assert msg_out.is_encrypted()
lp.sec("ac2: read message and check that it is encrypted")
lp.sec("ac2: read message and check that it's a verified chat")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
lp.sec("ac2: send message and let ac1 read it")
chat2.send_text("world")
msg = ac1._evtracker.wait_next_incoming_message()
@@ -103,13 +109,23 @@ def test_qr_group_join_and_chatting(acfactory, lp):
assert ch.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("ac1: add ac3 to the group")
lp.sec("ac1: add ac3 to verified group")
chat1.add_contact(ac3)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.is_system_message()
assert not msg.error
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
for ac2_contact in chat2.get_contacts():
if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF:
continue
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert ac2.get_self_contact().get_verifier(ac2_contact) is None
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
# System message about the added member.
@@ -179,10 +195,10 @@ def test_ephemeral_timer(acfactory, lp):
assert chat1.get_ephemeral_timer() == 0
def test_see_new_member_after_going_online(acfactory, tmp_path, lp):
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
"""The test for the bug #3836:
- Alice has two devices, the second is offline.
- Alice creates a group and sends a QR invitation to Bob.
- Alice creates a verified group and sends a QR invitation to Bob.
- Bob joins the group and sends a message there. Alice sees it.
- Alice's second devices goes online, but doesn't see Bob in the group.
"""
@@ -199,7 +215,7 @@ def test_see_new_member_after_going_online(acfactory, tmp_path, lp):
ac1_offl.import_self_keys(str(dir))
ac1_offl.stop_io()
lp.sec("ac1: create group QR, ac2 scans and joins")
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
@@ -208,7 +224,7 @@ def test_see_new_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
# "You were added by <addr>." message.
# "Member Me (<addr>) added by <addr>." message.
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.is_system_message()
msg_out = chat2.send_text("hello")
@@ -226,12 +242,12 @@ def test_see_new_member_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac2_addr
def test_use_new_group_after_going_online(acfactory, data, tmp_path, lp):
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
"""Another test for the bug #3836:
- Bob has two devices, the second is offline.
- Alice creates a group and sends a QR invitation to Bob.
- Alice creates a verified group and sends a QR invitation to Bob.
- Bob joins the group.
- Bob's second devices goes online, but sees a contact request instead of the group.
- Bob's second devices goes online, but sees a contact request instead of the verified group.
- The "member added" message is not a system message but a plain text message.
- Bob's second device doesn't display the Alice's avatar (bug #5354).
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
@@ -253,7 +269,7 @@ def test_use_new_group_after_going_online(acfactory, data, tmp_path, lp):
avatar_path = data.get_path("d.png")
ac1.set_avatar(avatar_path)
lp.sec("ac1: create group QR, ac2 scans and joins")
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
@@ -262,7 +278,7 @@ def test_use_new_group_after_going_online(acfactory, data, tmp_path, lp):
lp.sec("ac2_offl: going online, checking the 'member added' message")
ac2_offl.start_io()
# Receive "You were added by <addr>." message.
# Receive "Member Me (<addr>) added by <addr>." message.
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
contact = msg_in.get_sender_contact()
assert msg_in.is_system_message()

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