mirror of
https://github.com/chatmail/core.git
synced 2026-09-21 20:51:23 +03:00
Compare commits
227 Commits
link2xt/ru
...
link2xt/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ba32ce88c | ||
|
|
fae6a63023 | ||
|
|
54e16429b8 | ||
|
|
946120e427 | ||
|
|
e1d58706ce | ||
|
|
7876115d86 | ||
|
|
0a7e697f50 | ||
|
|
06150d6dc2 | ||
|
|
310dda494c | ||
|
|
8f39d7510f | ||
|
|
44ab013351 | ||
|
|
f7c97d8557 | ||
|
|
78caa6f53c | ||
|
|
349c9650e3 | ||
|
|
96e84d85a5 | ||
|
|
57b2c000e6 | ||
|
|
8014c2af2f | ||
|
|
5db55ac4de | ||
|
|
44c2febbe1 | ||
|
|
555b8b8b51 | ||
|
|
dc9bba0697 | ||
|
|
3858c47ceb | ||
|
|
6b1d982cfc | ||
|
|
5a71fe101e | ||
|
|
c41657d7fd | ||
|
|
6c3a866892 | ||
|
|
5007fc49c1 | ||
|
|
b1da53a56b | ||
|
|
d8912a98ac | ||
|
|
373f1840a6 | ||
|
|
81982273e8 | ||
|
|
5be489790f | ||
|
|
81ca5aa1ca | ||
|
|
15488ed210 | ||
|
|
f03088f247 | ||
|
|
d4115e720d | ||
|
|
abb0a119cb | ||
|
|
63364da79c | ||
|
|
a8e1110bcd | ||
|
|
dced877c90 | ||
|
|
44387f5e58 | ||
|
|
0b2051cda4 | ||
|
|
9e8c3a63ad | ||
|
|
f725e91b2a | ||
|
|
a2d8ced169 | ||
|
|
75a80fbb65 | ||
|
|
58b5f5d0f4 | ||
|
|
51b6d50a22 | ||
|
|
61d73433ea | ||
|
|
4232b8b20d | ||
|
|
f4992a3b78 | ||
|
|
187c5f4949 | ||
|
|
4bf42c0af9 | ||
|
|
b0e5b08531 | ||
|
|
49c30ecc75 | ||
|
|
506a78b5d4 | ||
|
|
a516829171 | ||
|
|
5ebab3d859 | ||
|
|
def14306dd | ||
|
|
d004c74a1a | ||
|
|
401626d674 | ||
|
|
477290972a | ||
|
|
77dd9075ca | ||
|
|
0b0398561d | ||
|
|
3ff8011676 | ||
|
|
38d6cf2bcd | ||
|
|
120bf11f4c | ||
|
|
5d02e92c11 | ||
|
|
76e3356e76 | ||
|
|
61dcc630fe | ||
|
|
d89d3ac9cd | ||
|
|
be693aacd9 | ||
|
|
3a95717783 | ||
|
|
504cb4b924 | ||
|
|
46d45faf7e | ||
|
|
70a01a6813 | ||
|
|
3d61e0f349 | ||
|
|
f162749dfe | ||
|
|
370cc5c1fd | ||
|
|
08b2374fcf | ||
|
|
5176a9c633 | ||
|
|
4d5ebe9ff4 | ||
|
|
322dd11d99 | ||
|
|
2f6f48c2a9 | ||
|
|
0517beef61 | ||
|
|
693c404266 | ||
|
|
a344cab046 | ||
|
|
96f051a2f0 | ||
|
|
9a33555604 | ||
|
|
3af45c306a | ||
|
|
709c56a889 | ||
|
|
7421e67ab6 | ||
|
|
49496756e4 | ||
|
|
ea3ca3a213 | ||
|
|
6c47e86380 | ||
|
|
fe25e93d6c | ||
|
|
0b2ff5d0e2 | ||
|
|
48888898ee | ||
|
|
c9880d0ba6 | ||
|
|
d2286254b3 | ||
|
|
589a628cda | ||
|
|
3df1e3712a | ||
|
|
f09f8fd8a4 | ||
|
|
357c6e74d5 | ||
|
|
484d56f24d | ||
|
|
8f3337d971 | ||
|
|
1283b986df | ||
|
|
33b258302b | ||
|
|
307ff9def1 | ||
|
|
0119db85ad | ||
|
|
b1c3615431 | ||
|
|
aa95d87568 | ||
|
|
07e18c64af | ||
|
|
e322fdf157 | ||
|
|
3dce06b02b | ||
|
|
40faf829d3 | ||
|
|
fbbe56c8ff | ||
|
|
ce7c8dbca1 | ||
|
|
0f8c91570b | ||
|
|
feb777f704 | ||
|
|
28a1a1da96 | ||
|
|
7add59fa76 | ||
|
|
e0494b0b37 | ||
|
|
412ed15b8b | ||
|
|
c1e961d37f | ||
|
|
1edbfa1024 | ||
|
|
56a99c227a | ||
|
|
31c37848ec | ||
|
|
ec4d195814 | ||
|
|
1a959c392b | ||
|
|
e94e33f59a | ||
|
|
e2b0eb1fb9 | ||
|
|
3363f63316 | ||
|
|
6beced4f51 | ||
|
|
cc229372e4 | ||
|
|
292bd9f58d | ||
|
|
998892395a | ||
|
|
9f5fbcdcae | ||
|
|
d89a972a2c | ||
|
|
ae5b27fb69 | ||
|
|
1a41e42bda | ||
|
|
c680432c23 | ||
|
|
4623d89528 | ||
|
|
5ec1d65294 | ||
|
|
8df4fe1c17 | ||
|
|
c3563f73e4 | ||
|
|
e15820fbd5 | ||
|
|
68ce93c420 | ||
|
|
fd8c56894a | ||
|
|
97932fda07 | ||
|
|
cd42efb36d | ||
|
|
2cacdbfd4b | ||
|
|
67437c946b | ||
|
|
6934aa26ae | ||
|
|
c3bc26d52f | ||
|
|
805e555a99 | ||
|
|
137ac9a027 | ||
|
|
a7632fe356 | ||
|
|
476fbf72e3 | ||
|
|
a6f33d7474 | ||
|
|
bd0c0b6b4b | ||
|
|
1247d5da36 | ||
|
|
93d9a4be1c | ||
|
|
b13b150997 | ||
|
|
bd846c6e43 | ||
|
|
547d22a3e3 | ||
|
|
7990e17859 | ||
|
|
487d33f4d7 | ||
|
|
e45667b547 | ||
|
|
5eab324751 | ||
|
|
15387690c8 | ||
|
|
ba35814d34 | ||
|
|
2ac217ddd4 | ||
|
|
8e60ed7d89 | ||
|
|
fab974f31e | ||
|
|
154e47dace | ||
|
|
99d202a973 | ||
|
|
ae02b6dcfc | ||
|
|
e1f5858fa3 | ||
|
|
abfc1b9a76 | ||
|
|
77d2d037f2 | ||
|
|
057d6c0c5e | ||
|
|
21e08d7e95 | ||
|
|
03c1ea9942 | ||
|
|
cf5f87c81b | ||
|
|
7021a3e899 | ||
|
|
a794e5f359 | ||
|
|
f7ffb924bf | ||
|
|
61f7107d0f | ||
|
|
710e33bc48 | ||
|
|
2df5ea038b | ||
|
|
dc267fe1fb | ||
|
|
62d3f3877f | ||
|
|
fb4fdeb470 | ||
|
|
db13d08f6c | ||
|
|
9476f9ec7f | ||
|
|
020e477662 | ||
|
|
3d50f3c9a3 | ||
|
|
35555ca753 | ||
|
|
0bb3d88bc6 | ||
|
|
e74ffb6e93 | ||
|
|
bbcfa5e40e | ||
|
|
7223d7ed74 | ||
|
|
733bec326a | ||
|
|
2d7af15124 | ||
|
|
00e1d00dfa | ||
|
|
856ea29c47 | ||
|
|
6a07a2b222 | ||
|
|
411ad16ea9 | ||
|
|
afb1d0dba8 | ||
|
|
df4197df56 | ||
|
|
9b7b32a536 | ||
|
|
682baff1b9 | ||
|
|
850ac0831f | ||
|
|
e2dd9b06e2 | ||
|
|
cc41a9ea27 | ||
|
|
dff8fdb293 | ||
|
|
85c407ab05 | ||
|
|
891a7f905d | ||
|
|
55ede9f2f6 | ||
|
|
7e5c1e8389 | ||
|
|
758fb9daa0 | ||
|
|
cd4850668f | ||
|
|
98a902b2de | ||
|
|
071358c605 | ||
|
|
82861a6903 | ||
|
|
ed6a335e9e |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -15,6 +15,6 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "monthly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.97.1
|
||||
RUST_VERSION: 1.98.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@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe
|
||||
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25
|
||||
with:
|
||||
arguments: --workspace --all-features --locked
|
||||
command: check
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
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@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@50414676f9f5d50a65992c6dd2ed02641263226c
|
||||
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
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@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
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@v6
|
||||
uses: actions/setup-python@v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
|
||||
12
.github/workflows/deltachat-rpc-server.yml
vendored
12
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -370,6 +370,14 @@ 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:
|
||||
@@ -382,7 +390,7 @@ jobs:
|
||||
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
||||
|
||||
publish_npm_package:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
@@ -401,7 +409,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
@@ -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/typescript
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
run: npm install --ignore-scripts
|
||||
|
||||
- name: Package
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
run: |
|
||||
npm run build
|
||||
npm pack .
|
||||
|
||||
- name: Publish
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||
|
||||
10
.github/workflows/jsonrpc.yml
vendored
10
.github/workflows/jsonrpc.yml
vendored
@@ -25,21 +25,21 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Add Rust cache
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
- name: npm install
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
run: npm install
|
||||
- name: Build TypeScript, run Rust tests, generate bindings
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
run: npm run build
|
||||
- name: Run integration tests
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
run: npm run test
|
||||
env:
|
||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||
- name: Run linter
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
run: npm run prettier:check
|
||||
|
||||
4
.github/workflows/nix.yml
vendored
4
.github/workflows/nix.yml
vendored
@@ -5,11 +5,13 @@ on:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- nix/**
|
||||
- .github/workflows/nix.yml
|
||||
push:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- nix/**
|
||||
- .github/workflows/nix.yml
|
||||
branches:
|
||||
- main
|
||||
@@ -26,7 +28,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- run: nix fmt flake.nix -- --check
|
||||
- run: nix fmt flake.nix nix/ -- --check
|
||||
|
||||
build:
|
||||
name: nix build
|
||||
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
working-directory: ./deltachat-jsonrpc-bindings/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/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-bindings/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
|
||||
|
||||
build-cffi:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -23,4 +23,4 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||
|
||||
290
CHANGELOG.md
290
CHANGELOG.md
@@ -1,5 +1,291 @@
|
||||
# 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
|
||||
|
||||
- [**breaking**] remove heartbeat push notifications.
|
||||
- [**breaking**] remove provider-db handling and provider lookup APIs.
|
||||
- provider lookup APIs were removed from CFFI and JSON-RPC.
|
||||
|
||||
also removes offline provider database code and generated provider data,
|
||||
provider-specific fields in configure/transport paths, and REPL providerinfo.
|
||||
|
||||
### Documentation
|
||||
|
||||
- remove oauth2 from standards.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- accept messages from key contacts with forged From address.
|
||||
- enable TLS certificate compression.
|
||||
- read SMTP recipient limit from relay IMAP metadata.
|
||||
|
||||
### Fixes
|
||||
|
||||
- fixup CI failures.
|
||||
- never merge outer To headers if standard header protection is used.
|
||||
- Re-add oauth2 to serialized structs ([#8464](https://github.com/chatmail/core/pull/8464)).
|
||||
- migrate transports configured on 2.56 to also have a oauth:false flag.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.57.0-dev.
|
||||
- deps: bump actions/setup-node from 6 to 7.
|
||||
- deps: bump cachix/install-nix-action from 31.10.6 to 31.11.0.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.20 to 2.1.1.
|
||||
- deps: bump taiki-e/install-action from 2.82.10 to 2.83.4.
|
||||
- cargo: bump quinn-proto from 0.11.14 to 0.11.16.
|
||||
|
||||
## [2.56.0] - 2026-07-21
|
||||
|
||||
### API-Changes
|
||||
@@ -8504,3 +8790,7 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.54.0]: https://github.com/chatmail/core/compare/v2.53.0..v2.54.0
|
||||
[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
|
||||
|
||||
@@ -2,45 +2,59 @@ 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(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/$ENV{CARGO_BUILD_TARGET}/release")
|
||||
else()
|
||||
set(ARCH_DIR "./")
|
||||
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/release")
|
||||
endif()
|
||||
|
||||
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
|
||||
)
|
||||
if(WITH_JSONRPC_BINDINGS)
|
||||
set(JSONRPC_ARGS --package deltachat-jsonrpc-bindings)
|
||||
endif()
|
||||
|
||||
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"
|
||||
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}"
|
||||
)
|
||||
|
||||
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)
|
||||
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")
|
||||
|
||||
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()
|
||||
|
||||
302
Cargo.lock
generated
302
Cargo.lock
generated
@@ -124,9 +124,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.103"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
@@ -194,15 +194,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.6.3"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08648fef353ab39a9d26f909ad53fc4f071be4c91853b78523f5cc3d9e5ebffd"
|
||||
checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"libc",
|
||||
"portable-atomic",
|
||||
"rustc-hash",
|
||||
"rustix 0.38.44",
|
||||
"rustix 1.1.4",
|
||||
"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.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@@ -327,7 +327,7 @@ dependencies = [
|
||||
"log",
|
||||
"nom 8.0.0",
|
||||
"pin-project",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -363,7 +363,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"futures-lite",
|
||||
"pin-project",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@@ -459,7 +459,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -497,11 +497,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.5"
|
||||
version = "1.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
|
||||
checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
@@ -698,9 +697,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.0"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -803,6 +802,17 @@ dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20poly1305"
|
||||
version = "0.10.1"
|
||||
@@ -810,7 +820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20",
|
||||
"chacha20 0.9.1",
|
||||
"cipher",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
@@ -1185,7 +1195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20",
|
||||
"chacha20 0.9.1",
|
||||
"crypto_secretbox",
|
||||
"curve25519-dalek",
|
||||
"salsa20",
|
||||
@@ -1201,7 +1211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20",
|
||||
"chacha20 0.9.1",
|
||||
"cipher",
|
||||
"generic-array",
|
||||
"poly1305",
|
||||
@@ -1302,9 +1312,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||
|
||||
[[package]]
|
||||
name = "dbl"
|
||||
@@ -1317,7 +1327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1389,7 +1399,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"testdir",
|
||||
"textwrap",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tokio-io-timeout",
|
||||
"tokio-rustls",
|
||||
@@ -1425,7 +1435,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1444,9 +1454,16 @@ dependencies = [
|
||||
"yerpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.61.0-dev"
|
||||
dependencies = [
|
||||
"deltachat-jsonrpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1462,7 +1479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1486,12 +1503,12 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"syn 3.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1501,7 +1518,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"rand 0.9.4",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"yerpc",
|
||||
]
|
||||
@@ -2110,9 +2127,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -2138,9 +2155,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -2163,15 +2180,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -2180,9 +2197,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -2199,32 +2216,32 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"syn 3.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -2283,11 +2300,25 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.2.0",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
@@ -2345,9 +2376,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -2434,7 +2465,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -2457,7 +2488,7 @@ dependencies = [
|
||||
"rand 0.9.4",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2556,9 +2587,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -3014,7 +3045,7 @@ dependencies = [
|
||||
"strum 0.26.2",
|
||||
"stun-rs",
|
||||
"surge-ping",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -3039,7 +3070,7 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -3081,7 +3112,7 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"serde-error",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -3126,7 +3157,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3146,7 +3177,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3201,7 +3232,7 @@ dependencies = [
|
||||
"sha1",
|
||||
"strum 0.26.2",
|
||||
"stun-rs",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
@@ -3291,9 +3322,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -3367,9 +3398,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@@ -3413,9 +3444,9 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
|
||||
|
||||
[[package]]
|
||||
name = "mail-builder"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900998f307338c4013a28ab14d760b784067324b164448c6d98a89e44810473b"
|
||||
checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
|
||||
|
||||
[[package]]
|
||||
name = "mailparse"
|
||||
@@ -3686,7 +3717,7 @@ dependencies = [
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-sys",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4175,7 +4206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@@ -4351,7 +4382,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sha1_smol",
|
||||
"simple-dns",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -4749,27 +4780,28 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
version = "0.11.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.3",
|
||||
"getrandom 0.4.3",
|
||||
"lru-slab",
|
||||
"rand 0.9.4",
|
||||
"rand 0.10.2",
|
||||
"rand_pcg",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -4820,6 +4852,12 @@ version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
@@ -4857,6 +4895,17 @@ dependencies = [
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20 0.10.2",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
@@ -4895,6 +4944,21 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||
dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.4.0"
|
||||
@@ -4976,14 +5040,14 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.4"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -4993,9 +5057,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -5218,9 +5282,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
version = "0.23.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"brotli-decompressor",
|
||||
@@ -5228,7 +5292,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.13",
|
||||
"rustls-webpki 0.103.15",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5265,9 +5329,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -5463,9 +5527,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
@@ -5482,22 +5546,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"syn 3.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5513,9 +5577,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -5653,7 +5717,7 @@ dependencies = [
|
||||
"shadowsocks-crypto",
|
||||
"socket2 0.5.9",
|
||||
"spin 0.10.1",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tokio-tfo",
|
||||
"trait-variant",
|
||||
@@ -6003,6 +6067,17 @@ 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"
|
||||
@@ -6097,7 +6172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.3",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.1",
|
||||
@@ -6140,11 +6215,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.18",
|
||||
"thiserror-impl 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6160,13 +6235,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"syn 3.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6248,9 +6323,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -6325,15 +6400,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
version = "0.7.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
@@ -6687,11 +6763,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.20.0"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
@@ -7353,7 +7429,7 @@ dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 2.0.20",
|
||||
"windows 0.59.0",
|
||||
"windows-core 0.59.0",
|
||||
]
|
||||
@@ -7459,9 +7535,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yerpc"
|
||||
version = "0.6.4"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dc24983fbe850227bfc1de89bf8cbfb3e2463afc322e0de2f155c4c23d06445"
|
||||
checksum = "6924db1f1011f1d22566c45c7090aa66c3107c3c463ca32beaa8b2327127040a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 1.9.0",
|
||||
@@ -7479,9 +7555,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yerpc_derive"
|
||||
version = "0.6.3"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d8560d021437420316370db865e44c000bf86380b47cf05e49be9d652042bf5"
|
||||
checksum = "42c374248c189b15a7f0660db3984e9dcc790ec15f4b44e932e92be49de829f9"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"darling",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.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.4.4", default-features = false }
|
||||
mail-builder = { version = "0.5.0", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.17"
|
||||
@@ -129,6 +129,7 @@ members = [
|
||||
"deltachat-ffi",
|
||||
"deltachat_derive",
|
||||
"deltachat-jsonrpc",
|
||||
"deltachat-jsonrpc-bindings",
|
||||
"deltachat-rpc-server",
|
||||
"deltachat-ratelimit",
|
||||
"deltachat-repl",
|
||||
@@ -202,7 +203,7 @@ thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.18"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
yerpc = "0.7"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -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-imap)
|
||||
and [IMAP](https://github.com/chatmail/async-smtp) handling
|
||||
- robust [SMTP](https://github.com/chatmail/async-smtp)
|
||||
and [IMAP](https://github.com/chatmail/async-imap) 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 cargo-bolero
|
||||
$ cargo install --locked cargo-bolero
|
||||
```
|
||||
|
||||
Run fuzzing tests with
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "deltachat-contact-tools"
|
||||
version = "0.0.0" # No semver-stable versioning
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
||||
license = "MPL-2.0"
|
||||
|
||||
|
||||
@@ -29,14 +29,13 @@
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use anyhow::bail;
|
||||
use regex::regex;
|
||||
|
||||
mod vcard;
|
||||
pub use vcard::{make_vcard, parse_vcard, VcardContact};
|
||||
pub use vcard::{VcardContact, make_vcard, parse_vcard};
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -88,9 +87,7 @@ impl rusqlite::types::ToSql for ContactAddress {
|
||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
||||
/// - Removes the name if it is equal to the address by setting it to ""
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
let (name, addr) = if let Some(captures) = regex!("(.*)<(.*)>").captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
captures.get(1).map_or("", |m| m.as_str())
|
||||
@@ -107,7 +104,7 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
let mut name = sanitize_name(name);
|
||||
|
||||
// If the 'display name' is just the address, remove it:
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)".
|
||||
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||
if name == addr {
|
||||
name = "".to_string();
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use chrono::DateTime;
|
||||
use chrono::NaiveDateTime;
|
||||
use regex::Regex;
|
||||
use regex::regex;
|
||||
|
||||
use crate::sanitize_name_and_addr;
|
||||
|
||||
@@ -210,9 +208,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
}
|
||||
|
||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||
static NEWLINE_AND_SPACE_OR_TAB: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||
let unfolded_lines = regex!("\r?\n[\t ]").replace_all(vcard, "");
|
||||
|
||||
let mut lines = unfolded_lines.lines().peekable();
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
@@ -220,7 +220,10 @@ END:VCARD
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
assert_eq!(
|
||||
contacts[0].profile_image.as_deref().unwrap(),
|
||||
"/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +247,10 @@ END:VCARD",
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
assert_eq!(
|
||||
contacts[0].key.as_ref().unwrap(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
);
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
@@ -272,9 +278,15 @@ END:VCARD",
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice");
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
|
||||
assert_eq!(
|
||||
contacts[0].key.as_ref().unwrap(),
|
||||
"xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=="
|
||||
);
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||
assert_eq!(
|
||||
contacts[0].profile_image.as_ref().unwrap(),
|
||||
"/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
|
||||
@@ -453,18 +453,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* always auto-downloaded.
|
||||
* 0 = no limit (default).
|
||||
* Changes affect future messages only.
|
||||
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
|
||||
* This is an experimental option not compatible to other MUAs
|
||||
* and older Delta Chat versions.
|
||||
* 1 = enable.
|
||||
* 0 = disable (default).
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `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()`,
|
||||
@@ -526,9 +517,10 @@ 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,
|
||||
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
|
||||
* 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.
|
||||
* - `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.
|
||||
*
|
||||
@@ -600,13 +592,10 @@ 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-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.
|
||||
* - 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
|
||||
*
|
||||
* Meant as a rough overview that can be shown
|
||||
* e.g. in the title of the main screen.
|
||||
@@ -638,22 +627,6 @@ int dc_get_connectivity (dc_context_t* context);
|
||||
char* dc_get_connectivity_html (dc_context_t* context);
|
||||
|
||||
|
||||
#define DC_PUSH_NOT_CONNECTED 0
|
||||
#define DC_PUSH_CONNECTED 2
|
||||
|
||||
/**
|
||||
* Get the current push notification state.
|
||||
* One of:
|
||||
* - DC_PUSH_NOT_CONNECTED
|
||||
* - DC_PUSH_CONNECTED
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return Push notification state.
|
||||
*/
|
||||
int dc_get_push_state (dc_context_t* context);
|
||||
|
||||
|
||||
// connect
|
||||
|
||||
/**
|
||||
@@ -715,6 +688,21 @@ int dc_get_push_state (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.
|
||||
*
|
||||
@@ -888,14 +876,14 @@ uint32_t dc_create_chat_by_contact_id (dc_context_t* context, uint32_t co
|
||||
|
||||
|
||||
/**
|
||||
* Check, if there is a normal chat with a given contact.
|
||||
* Check, if there is a single chat with a given contact.
|
||||
* To get the chat messages, use dc_get_chat_msgs().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param contact_id The contact ID to check.
|
||||
* @return If there is a normal chat with the given contact_id, this chat_id is
|
||||
* returned. If there is no normal chat with the contact_id, the function
|
||||
* @return If there is a single chat with the given contact_id, this chat_id is
|
||||
* returned. If there is no single chat with the contact_id, the function
|
||||
* returns 0.
|
||||
*/
|
||||
uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t contact_id);
|
||||
@@ -1192,7 +1180,7 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to place a call for.
|
||||
* This needs to be a one-to-one chat.
|
||||
* This needs to be a single chat.
|
||||
* @param place_call_info any data that other devices receive
|
||||
* in #DC_EVENT_INCOMING_CALL.
|
||||
* @param has_video Whether the call has video initially.
|
||||
@@ -1598,7 +1586,7 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
|
||||
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear
|
||||
* and the user may create the chat again.
|
||||
* - **Groups are not left** - this would
|
||||
* be unexpected as (1) deleting a normal chat also does not prevent new mails
|
||||
* be unexpected as (1) deleting a single chat also does not prevent new mails
|
||||
* from arriving, (2) leaving a group requires sending a message to
|
||||
* all group members - especially for groups not used for a longer time, this is
|
||||
* really unexpected when deletion results in contacting all members again,
|
||||
@@ -1616,7 +1604,7 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
|
||||
/**
|
||||
* Block a chat.
|
||||
*
|
||||
* Blocking 1:1 chats blocks the corresponding contact. Blocking
|
||||
* Blocking single chats blocks the corresponding contact. Blocking
|
||||
* mailing lists creates a pseudo-contact in the list of blocked
|
||||
* contacts, so blocked mailing lists can be discovered and unblocked
|
||||
* the same way as the contacts. Blocking group chats deletes the
|
||||
@@ -1645,7 +1633,7 @@ void dc_accept_chat (dc_context_t* context, uint32_t ch
|
||||
/**
|
||||
* Get the contact IDs belonging to a chat.
|
||||
*
|
||||
* - for normal chats, the function always returns exactly one contact,
|
||||
* - for single chats, the function always returns exactly one contact,
|
||||
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
||||
*
|
||||
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
||||
@@ -1796,8 +1784,6 @@ 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
|
||||
@@ -2083,7 +2069,7 @@ int dc_resend_msgs (dc_context_t* context, const uint3
|
||||
* The concrete action depends on the type of the chat and on the users settings
|
||||
* (dc_msgs_presented() may be a better name therefore, but well. :)
|
||||
*
|
||||
* - For normal chats, the IMAP state is updated, MDN is sent
|
||||
* - For single chats, the IMAP state is updated, MDN is sent
|
||||
* (if dc_set_config()-options `mdns_enabled` is set)
|
||||
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||
*
|
||||
@@ -2296,7 +2282,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 for a simple out-of-band verification.
|
||||
* fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -2463,7 +2449,7 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
|
||||
void dc_stop_ongoing_process (dc_context_t* context);
|
||||
|
||||
|
||||
// out-of-band verification
|
||||
// securejoin
|
||||
|
||||
#define DC_QR_ASK_VERIFYCONTACT 200 // id=contact
|
||||
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
|
||||
@@ -2498,7 +2484,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 verify the contact;
|
||||
* ask whether to start chatting with the contact;
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
* - DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
|
||||
@@ -2507,7 +2493,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 verified,
|
||||
* contact fingerprint matches,
|
||||
* ask the user if they want to start chatting;
|
||||
* if so, call dc_create_chat_by_contact_id().
|
||||
*
|
||||
@@ -2583,23 +2569,24 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
|
||||
|
||||
/**
|
||||
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
|
||||
* Get QR code text that will offer a SecureJoin 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
|
||||
* an out-of-band-verification can be joined using dc_join_securejoin()
|
||||
* the SecureJoin protocol can be started 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 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.
|
||||
* the SecureJoin QR code for the group is returned.
|
||||
* If set to 0, the setup contact QR code is returned.
|
||||
* See https://securejoin.delta.chat/
|
||||
* for details about both protocols.
|
||||
* @return The text that should go to the QR code,
|
||||
@@ -2625,7 +2612,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 a Setup-Contact or Verified-Group-Invite protocol
|
||||
* Continue the SecureJoin 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
|
||||
@@ -2645,7 +2632,6 @@ 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);
|
||||
|
||||
@@ -3207,19 +3193,33 @@ 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.
|
||||
*
|
||||
* dc_accounts_background_fetch() was created for the iOS Background fetch.
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
* Process all events until you get this one and you can safely return to the background
|
||||
* without forgetting to create notifications caused by timing race conditions.
|
||||
* 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.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @param timeout The timeout in seconds
|
||||
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
|
||||
* @return Return 0 if the call was ignored because `accounts` is NULL or the timeout is too small, 1 otherwise.
|
||||
*/
|
||||
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
||||
|
||||
@@ -3574,19 +3574,11 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
|
||||
dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t chat_id, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to get the associated context object.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @deprecated 2026-08-13, use dedicated dc_chat_get_*() getters or jsonrpc
|
||||
*
|
||||
* The returned JSON string has the following key/values:
|
||||
*
|
||||
* id: chat id
|
||||
@@ -3597,7 +3589,6 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
|
||||
* 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);
|
||||
@@ -3667,7 +3658,7 @@ char* dc_chat_get_mailinglist_addr (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get name of a chat. For one-to-one chats, this is the name of the contact.
|
||||
* Get name of a chat. For single chats, this is the name of the contact.
|
||||
* For group chats, this is the name given e.g. to dc_create_group_chat() or
|
||||
* received by a group-creation message.
|
||||
*
|
||||
@@ -3684,7 +3675,7 @@ char* dc_chat_get_name (const dc_chat_t* chat);
|
||||
* Get the chat's profile image.
|
||||
* For groups, this is the image set by any group member
|
||||
* using dc_set_chat_profile_image().
|
||||
* For normal chats, this is the image set by each remote user on their own
|
||||
* For single chats, this is the image set by each remote user on their own
|
||||
* using dc_set_config(context, "selfavatar", image).
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -3698,7 +3689,7 @@ char* dc_chat_get_profile_image (const dc_chat_t* chat);
|
||||
|
||||
/**
|
||||
* Get a color for the chat.
|
||||
* For 1:1 chats, the color is calculated from the contact's e-mail address.
|
||||
* For single chats, the color is calculated from the contact's e-mail address.
|
||||
* Otherwise, the chat name is used.
|
||||
* The color can be used for an fallback avatar with white initials
|
||||
* as well as for headlines in bubbles of group chats.
|
||||
@@ -3765,7 +3756,7 @@ int dc_chat_is_unpromoted (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a chat is a self talk. Self talks are normal chats with
|
||||
* Check if a chat is a self talk. Self talks are single chats with
|
||||
* the only contact DC_CONTACT_ID_SELF.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -3806,23 +3797,12 @@ 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.
|
||||
*
|
||||
* 1:1 chats with key-contacts and group chats with key-contacts
|
||||
* Single chats with key-contacts and group chats with key-contacts
|
||||
* are encrypted.
|
||||
* 1:1 chats with emails contacts and ad-hoc groups
|
||||
* Single chats with emails contacts and ad-hoc groups
|
||||
* created for email threads are not encrypted.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
@@ -4356,16 +4336,16 @@ char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_c
|
||||
* display name, or NULL.
|
||||
*
|
||||
* If this returns non-NULL, put a `~` before the override-sender-name and show the
|
||||
* override-sender-name and the sender's avatar even in 1:1 chats.
|
||||
* override-sender-name and the sender's avatar even in single chats.
|
||||
*
|
||||
* In mailing lists, sender display name and sender address do not always belong together.
|
||||
* In this case, this function gives you the name that should actually be shown over the message.
|
||||
*
|
||||
* Also, sometimes, we need to indicate a different sender in 1:1 chats:
|
||||
* Also, sometimes, we need to indicate a different sender in single chats:
|
||||
* Suppose that our user writes an e-mail to support@delta.chat, which forwards to
|
||||
* Bob <bob@delta.chat>, and Bob replies.
|
||||
*
|
||||
* Then, Bob's reply is shown in our 1:1 chat with support@delta.chat and the override-sender-name is
|
||||
* Then, Bob's reply is shown in our single chat with support@delta.chat and the override-sender-name is
|
||||
* set to `Bob`. The UI should show the sender name as `~Bob` and show the avatar, just
|
||||
* as in group messages. If the user then taps on the avatar, they can see that this message
|
||||
* comes from bob@delta.chat.
|
||||
@@ -4499,6 +4479,7 @@ 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.
|
||||
@@ -4558,6 +4539,7 @@ 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
|
||||
|
||||
|
||||
/**
|
||||
@@ -4875,6 +4857,8 @@ 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().
|
||||
*
|
||||
@@ -4914,6 +4898,20 @@ 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
|
||||
*
|
||||
@@ -4929,7 +4927,6 @@ uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
* By default, these names are equal,
|
||||
* but functions working with contact names
|
||||
* (e.g. dc_contact_get_name(), dc_contact_get_display_name(),
|
||||
* dc_contact_get_name_n_addr(),
|
||||
* dc_create_contact() or dc_add_address_book())
|
||||
* only affect the given-name.
|
||||
*/
|
||||
@@ -4979,7 +4976,7 @@ char* dc_contact_get_addr (const dc_contact_t* contact);
|
||||
* The function does not return the contact name as received from the network.
|
||||
*
|
||||
* This name is typically used in a form where the user can edit the name of a contact.
|
||||
* To get a fine name to display in lists etc., use dc_contact_get_display_name() or dc_contact_get_name_n_addr().
|
||||
* To get a fine name to display in lists etc., use dc_contact_get_display_name().
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5034,23 +5031,6 @@ char* dc_contact_get_display_name (const dc_contact_t* contact);
|
||||
#define dc_contact_get_first_name dc_contact_get_display_name
|
||||
|
||||
|
||||
/**
|
||||
* Get a summary of name and address.
|
||||
*
|
||||
* The returned string is either "Name (email@domain.com)" or just
|
||||
* "email@domain.com" if the name is unset.
|
||||
*
|
||||
* The summary is typically used when asking the user something about the contact.
|
||||
* The attached e-mail address makes the question unique, e.g. "Chat with Alan Miller (am@uniquedomain.com)?"
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return A summary string, must be released using dc_str_unref().
|
||||
* Never returns NULL.
|
||||
*/
|
||||
char* dc_contact_get_name_n_addr (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Get the contact's profile image.
|
||||
* This is the image set by each remote user on their own
|
||||
@@ -5130,19 +5110,6 @@ int dc_contact_was_seen_recently (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.
|
||||
*
|
||||
@@ -5167,36 +5134,6 @@ 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
|
||||
*
|
||||
@@ -5510,7 +5447,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_CHAT_TYPE_UNDEFINED 0
|
||||
|
||||
/**
|
||||
* A one-to-one chat with a single contact.
|
||||
* A single chat with a single contact.
|
||||
*
|
||||
* dc_get_chat_contacts() contains one record for the user.
|
||||
* DC_CONTACT_ID_SELF is added _only_ for a self talk.
|
||||
@@ -5698,6 +5635,7 @@ 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().
|
||||
@@ -6226,7 +6164,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
* Contact(s) created, renamed, blocked or deleted.
|
||||
*
|
||||
* @param data1 (int) contact_id of the changed contact or 0 on batch-changes or deletion.
|
||||
* @param data2 0
|
||||
@@ -6298,8 +6236,7 @@ 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 "alice@addr verified, introducing myself."
|
||||
* (Bob has verified alice and waits until Alice does the same for him)
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
|
||||
* 1000=vg-member-added/vc-contact-confirm received
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||
@@ -6382,11 +6319,18 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -6482,7 +6426,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 1 minute timeout.
|
||||
* Moreover, the event is sent when the call was not accepted within two minutes.
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
@@ -6494,9 +6438,10 @@ 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 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)`.
|
||||
* 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)`.
|
||||
*/
|
||||
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
||||
|
||||
@@ -6681,21 +6626,12 @@ 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
|
||||
|
||||
/// "Cannot login as %1$s."
|
||||
///
|
||||
/// Used in error strings.
|
||||
/// - %1$s will be replaced by the failing login name
|
||||
/// @deprecated 2026-08-24
|
||||
#define DC_STR_CANNOT_LOGIN 60
|
||||
|
||||
/// "Location streaming enabled."
|
||||
@@ -6740,7 +6676,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Message from %1$s"
|
||||
///
|
||||
/// Used in subjects of outgoing messages in one-to-one chats.
|
||||
/// Used in subjects of outgoing messages in single chats.
|
||||
/// - %1$s will be replaced by the name of the sender,
|
||||
/// this is the dc_set_config()-option `displayname` or `addr`
|
||||
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
|
||||
@@ -6868,7 +6804,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, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
|
||||
/// #DC_STR_SECURE_JOIN_REPLIES and #DC_STR_MSGADDMEMBER.
|
||||
///
|
||||
/// `%1$s` and `%2$s` will be replaced by name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_STARTED 117
|
||||
@@ -6877,15 +6813,13 @@ 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 verification qrcode svg image generated by the core.
|
||||
/// Subtitle for the invite 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
|
||||
@@ -7180,6 +7114,30 @@ 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.
|
||||
@@ -7275,6 +7233,15 @@ 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
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::chat::ChatItem;
|
||||
use crate::constants::DC_MSG_ID_DAYMARKER;
|
||||
use crate::contact::ContactId;
|
||||
use crate::location::Location;
|
||||
use crate::message::MsgId;
|
||||
@@ -21,7 +20,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 { .. } => DC_MSG_ID_DAYMARKER,
|
||||
ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
|
||||
},
|
||||
Self::Locations(array) => array[index].location_id,
|
||||
Self::Uint(array) => array[index],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,13 +17,15 @@ use std::ptr;
|
||||
/// }
|
||||
/// ```
|
||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
let ret: *mut libc::c_char = if !s.is_null() {
|
||||
libc::strdup(s)
|
||||
} else {
|
||||
libc::calloc(1, 1) as *mut libc::c_char
|
||||
};
|
||||
assert!(!ret.is_null());
|
||||
ret
|
||||
unsafe {
|
||||
let ret: *mut libc::c_char = if !s.is_null() {
|
||||
libc::strdup(s)
|
||||
} else {
|
||||
libc::calloc(1, 1) as *mut libc::c_char
|
||||
};
|
||||
assert!(!ret.is_null());
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for the [OsStrExt] trait
|
||||
@@ -164,34 +166,40 @@ pub(crate) trait Strdup {
|
||||
/// This function will panic when the original string contains an
|
||||
/// interior null byte as this can not be represented in raw C
|
||||
/// strings.
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl Strdup for str {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for String {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
let s: &str = self;
|
||||
s.strdup()
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for std::path::Path {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Strdup for [u8] {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
unsafe {
|
||||
let tmp = CString::new_lossy(self);
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,15 +217,15 @@ pub(crate) trait OptStrdup {
|
||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||
///
|
||||
/// See [Strdup::strdup] for details.
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
fn strdup(&self) -> *mut libc::c_char {
|
||||
match self {
|
||||
Some(s) => {
|
||||
let tmp = CString::new_lossy(s.as_ref());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
unsafe { dc_strdup(tmp.as_ptr()) }
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -255,20 +263,18 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
///
|
||||
/// [Path]: std::path::Path
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
unsafe {
|
||||
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
|
||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
||||
std::path::Path::new(os_str)
|
||||
}
|
||||
let c_str = unsafe { std::ffi::CStr::from_ptr(s) }.to_bytes();
|
||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
||||
std::path::Path::new(os_str)
|
||||
}
|
||||
|
||||
// as_path() implementation for windows, documented above.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
as_path_unicode(s)
|
||||
pub(crate) unsafe fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
unsafe { as_path_unicode(s) }
|
||||
}
|
||||
|
||||
// Implementation for as_path() on Windows.
|
||||
@@ -276,7 +282,7 @@ pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
// Having this as a separate function means it can be tested on unix
|
||||
// too.
|
||||
#[allow(dead_code)]
|
||||
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
unsafe fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
|
||||
let cstr = unsafe { CStr::from_ptr(s) };
|
||||
@@ -364,14 +370,20 @@ mod tests {
|
||||
fn test_as_path() {
|
||||
let some_path = CString::new("/some/path").unwrap();
|
||||
let ptr = some_path.as_ptr();
|
||||
assert_eq!(as_path(ptr), std::ffi::OsString::from("/some/path"))
|
||||
assert_eq!(
|
||||
unsafe { as_path(ptr) },
|
||||
std::ffi::OsString::from("/some/path")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_path_unicode_fn() {
|
||||
let some_path = CString::new("/some/path").unwrap();
|
||||
let ptr = some_path.as_ptr();
|
||||
assert_eq!(as_path_unicode(ptr), std::ffi::OsString::from("/some/path"));
|
||||
assert_eq!(
|
||||
unsafe { as_path_unicode(ptr) },
|
||||
std::ffi::OsString::from("/some/path")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
16
deltachat-jsonrpc-bindings/Cargo.toml
Normal file
16
deltachat-jsonrpc-bindings/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[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"]
|
||||
7
deltachat-jsonrpc-bindings/build.rs
Normal file
7
deltachat-jsonrpc-bindings/build.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
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");
|
||||
}
|
||||
1
deltachat-jsonrpc-bindings/qt/.gitignore
vendored
Normal file
1
deltachat-jsonrpc-bindings/qt/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
generated
|
||||
111
deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp
Normal file
111
deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#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 *)
|
||||
1
deltachat-jsonrpc-bindings/src/lib.rs
Normal file
1
deltachat-jsonrpc-bindings/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.57.0-dev"
|
||||
"version": "2.61.0-dev"
|
||||
}
|
||||
@@ -44,7 +44,6 @@ const constants = data
|
||||
key.startsWith("DC_CERTCK_") ||
|
||||
key.startsWith("DC_SOCKET_") ||
|
||||
key.startsWith("DC_LP_AUTH_") ||
|
||||
key.startsWith("DC_PUSH_") ||
|
||||
key.startsWith("DC_TEXT1_") ||
|
||||
key.startsWith("DC_CHAT_TYPE")
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
|
||||
@@ -5,26 +5,26 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use deltachat::EventEmitter;
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
|
||||
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
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,
|
||||
marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use deltachat::config::{Config, get_all_ui_config_keys};
|
||||
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, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
|
||||
markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ext, get_existing_msg_ids,
|
||||
get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs,
|
||||
};
|
||||
use deltachat::peer_channels::{
|
||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||
@@ -36,10 +36,9 @@ use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use tokio::sync::{Mutex, RwLock, watch};
|
||||
use types::login_param::EnteredLoginParam;
|
||||
use yerpc::rpc;
|
||||
|
||||
@@ -53,7 +52,6 @@ use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||
use types::notify_state::JsonrpcNotifyState;
|
||||
use types::reactions::JsonrpcReactions;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
@@ -65,8 +63,8 @@ use self::types::{
|
||||
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
},
|
||||
};
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::login_param::TransportListEntry;
|
||||
use crate::api::types::appversions::JsonrpcAppSource;
|
||||
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -155,7 +153,7 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
#[rpc(all_positional)]
|
||||
impl CommandApi {
|
||||
/// Test function.
|
||||
async fn sleep(&self, delay: f64) {
|
||||
@@ -280,9 +278,26 @@ impl CommandApi {
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of 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.
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
/// 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.
|
||||
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
let future = {
|
||||
let lock = self.accounts.read().await;
|
||||
@@ -293,6 +308,11 @@ 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(())
|
||||
@@ -328,12 +348,6 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current push notification state.
|
||||
async fn get_push_state(&self, account_id: u32) -> Result<JsonrpcNotifyState> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.push_state().into())
|
||||
}
|
||||
|
||||
/// Get the combined filesize of an account in bytes
|
||||
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -509,8 +523,7 @@ 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::set_transport_unpublished()] to remove a transport.
|
||||
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
async fn add_or_update_transport(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -535,32 +548,8 @@ 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::set_transport_unpublished()] to remove a transport.
|
||||
/// and [Self::delete_transport()] 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()
|
||||
@@ -571,41 +560,17 @@ impl CommandApi {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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.
|
||||
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?;
|
||||
@@ -858,7 +823,7 @@ impl CommandApi {
|
||||
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
|
||||
/// and the user may create the chat again.
|
||||
/// - **Groups are not left** - this would
|
||||
/// be unexpected as (1) deleting a normal chat also does not prevent new mails
|
||||
/// be unexpected as (1) deleting a single chat also does not prevent new mails
|
||||
/// from arriving, (2) leaving a group requires sending a message to
|
||||
/// all group members - especially for groups not used for a longer time, this is
|
||||
/// really unexpected when deletion results in contacting all members again,
|
||||
@@ -882,6 +847,8 @@ 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(
|
||||
@@ -895,20 +862,19 @@ impl CommandApi {
|
||||
Ok(qr)
|
||||
}
|
||||
|
||||
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
|
||||
/// Get QR code (text and SVG) that will offer a SecureJoin 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`
|
||||
/// an out-of-band-verification can be joined using `secure_join()`
|
||||
/// the securejoin protocol can be started 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 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.
|
||||
/// the SecureJoin QR code for the group is returned.
|
||||
/// If not set, the setup contact QR code is returned.
|
||||
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||
///
|
||||
/// return format: `[code, svg]`
|
||||
@@ -924,7 +890,7 @@ impl CommandApi {
|
||||
Ok((qr, svg))
|
||||
}
|
||||
|
||||
/// Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
/// Continue the SecureJoin 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.
|
||||
@@ -942,7 +908,6 @@ 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?;
|
||||
@@ -1008,8 +973,6 @@ 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,
|
||||
@@ -1023,7 +986,7 @@ impl CommandApi {
|
||||
|
||||
/// Get the contact IDs belonging to a chat.
|
||||
///
|
||||
/// - for normal chats, the function always returns exactly one contact,
|
||||
/// - for single chats, the function always returns exactly one contact,
|
||||
/// DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
||||
///
|
||||
/// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
||||
@@ -1351,7 +1314,7 @@ impl CommandApi {
|
||||
/// The concrete action depends on the type of the chat and on the users settings
|
||||
/// (dc_msgs_presented() may be a better name therefore, but well. :)
|
||||
///
|
||||
/// - For normal chats, the IMAP state is updated, MDN is sent
|
||||
/// - For single chats, the IMAP state is updated, MDN is sent
|
||||
/// (if set_config()-options `mdns_enabled` is set)
|
||||
/// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||
///
|
||||
@@ -1389,7 +1352,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 `DC_MSG_ID_DAYMARKER` to the result,
|
||||
/// * `add_daymarker` - If `true`, add day markers as `MsgId::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(
|
||||
@@ -1400,7 +1363,7 @@ impl CommandApi {
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
let msg = get_chat_msgs_ext(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
@@ -1411,7 +1374,7 @@ impl CommandApi {
|
||||
.map(|chat_item| -> u32 {
|
||||
match chat_item {
|
||||
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
deltachat::chat::ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
@@ -1449,7 +1412,7 @@ impl CommandApi {
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<JsonrpcMessageListItem>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
let msg = get_chat_msgs_ext(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
@@ -1515,12 +1478,32 @@ 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_ex(&ctx, &msgs, false).await
|
||||
delete_msgs_ext(&ctx, &msgs, false).await
|
||||
}
|
||||
|
||||
/// Delete messages. The messages are deleted on the current device,
|
||||
@@ -1528,7 +1511,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_ex(&ctx, &msgs, true).await
|
||||
delete_msgs_ext(&ctx, &msgs, true).await
|
||||
}
|
||||
|
||||
/// Get an informational text for a single message. The text is multiline and may
|
||||
@@ -1811,7 +1794,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 for a simple out-of-band verification.
|
||||
/// fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
|
||||
async fn get_contact_encryption_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1893,7 +1876,7 @@ impl CommandApi {
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||
/// Returns the [`ChatId`] for the single chat with `contact_id` if it exists.
|
||||
///
|
||||
/// If it does not exist, `None` is returned.
|
||||
async fn get_chat_id_by_contact_id(
|
||||
@@ -2076,15 +2059,20 @@ 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-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.
|
||||
/// - 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
|
||||
///
|
||||
/// Meant as a rough overview that can be shown
|
||||
/// e.g. in the title of the main screen.
|
||||
@@ -2268,6 +2256,9 @@ 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,
|
||||
@@ -2278,7 +2269,7 @@ impl CommandApi {
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
let blob = message.get_webxdc_blob(&ctx, &path).await?;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||
}
|
||||
|
||||
@@ -2448,6 +2439,7 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Returns reactions to the message.
|
||||
/// `None` when there are no reactions.
|
||||
async fn get_message_reactions(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2779,6 +2771,48 @@ 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)
|
||||
|
||||
31
deltachat-jsonrpc/src/api/types/appversions.rs
Normal file
31
deltachat-jsonrpc/src/api/types/appversions.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{call_state, CallState};
|
||||
use deltachat::calls::{CallState, call_state};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::message::MsgId;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use deltachat::chat::{self, ChatVisibility, get_chat_contacts, get_past_chat_contacts};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
|
||||
@@ -4,7 +4,7 @@ use deltachat::chatlist::get_last_message_for_chat;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::{
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chat::{ChatVisibility, get_chat_contacts},
|
||||
chatlist::Chatlist,
|
||||
};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
|
||||
@@ -17,7 +17,6 @@ pub struct ContactObject {
|
||||
id: u32,
|
||||
name: String,
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
|
||||
/// Is the contact a key contact.
|
||||
@@ -31,37 +30,6 @@ 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,
|
||||
@@ -79,14 +47,6 @@ 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()),
|
||||
@@ -96,12 +56,9 @@ impl ContactObject {
|
||||
id: contact.id.to_u32(),
|
||||
name: contact.get_name().to_owned(),
|
||||
profile_image, //BLOBS
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
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(),
|
||||
|
||||
@@ -337,8 +337,7 @@ pub enum EventType {
|
||||
contact_id: u32,
|
||||
|
||||
/// Progress as:
|
||||
/// 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)
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: u16,
|
||||
},
|
||||
@@ -394,11 +393,15 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// 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.
|
||||
/// Tells that a background fetch call is done:
|
||||
/// the fetch completed, timed out, was stopped or was not started.
|
||||
///
|
||||
/// This event is only emitted by the account manager
|
||||
/// 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.
|
||||
AccountsBackgroundFetchDone,
|
||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
///
|
||||
@@ -478,9 +481,9 @@ pub enum EventType {
|
||||
///
|
||||
/// UI should update the list.
|
||||
///
|
||||
/// This event is emitted when transport
|
||||
/// synchronization messages arrives,
|
||||
/// but not when the UI modifies the transport list by itself.
|
||||
/// The event is emitted on the device modifying
|
||||
/// the transports as well as on other devices
|
||||
/// applying the synced change.
|
||||
TransportsModified,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct HttpResponse {
|
||||
|
||||
impl From<CoreHttpResponse> for HttpResponse {
|
||||
fn from(response: CoreHttpResponse) -> Self {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
|
||||
let mimetype = response.mimetype;
|
||||
let encoding = response.encoding;
|
||||
|
||||
@@ -4,16 +4,6 @@ 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`,
|
||||
@@ -68,15 +58,6 @@ 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();
|
||||
@@ -122,6 +103,7 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
||||
password: param.smtp_password.unwrap_or_default(),
|
||||
},
|
||||
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
||||
oauth2: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,9 @@ pub struct MessageObject {
|
||||
|
||||
saved_message_id: Option<u32>,
|
||||
|
||||
is_pinned: bool,
|
||||
|
||||
/// `None` when there are no reactions.
|
||||
reactions: Option<JsonrpcReactions>,
|
||||
|
||||
vcard_contact: Option<VcardContact>,
|
||||
@@ -263,6 +266,7 @@ impl MessageObject {
|
||||
.await?
|
||||
.map(|id| id.to_u32()),
|
||||
|
||||
is_pinned: message.is_pinned(),
|
||||
reactions,
|
||||
|
||||
vcard_contact: vcard_contacts.first().cloned(),
|
||||
@@ -390,11 +394,11 @@ pub enum SystemMessageType {
|
||||
LocationOnly,
|
||||
InvalidUnencryptedMail,
|
||||
|
||||
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
|
||||
/// Single chats info message telling that SecureJoin has started and the user should wait for it
|
||||
/// to complete.
|
||||
SecurejoinWait,
|
||||
|
||||
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
|
||||
/// Single chats info message telling that SecureJoin is still running, but the user may already
|
||||
/// send messages.
|
||||
SecurejoinWaitTimeout,
|
||||
|
||||
@@ -425,6 +429,8 @@ pub enum SystemMessageType {
|
||||
|
||||
CallAccepted,
|
||||
CallEnded,
|
||||
MessagePinned,
|
||||
MessageUnpinned,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
@@ -454,6 +460,8 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -726,9 +734,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 }
|
||||
}
|
||||
deltachat::ephemeral::Timer::Enabled { duration } => EphemeralTimer::Enabled {
|
||||
duration: duration.get(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod account;
|
||||
pub mod appversions;
|
||||
pub mod calls;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
@@ -8,8 +9,6 @@ pub mod http;
|
||||
pub mod location;
|
||||
pub mod login_param;
|
||||
pub mod message;
|
||||
pub mod notify_state;
|
||||
|
||||
pub mod qr;
|
||||
pub mod reactions;
|
||||
pub mod webxdc;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use deltachat::push::NotifyState;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "NotifyState")]
|
||||
pub enum JsonrpcNotifyState {
|
||||
/// Not subscribed to push notifications.
|
||||
NotConnected,
|
||||
|
||||
/// Subscribed to push notifications for new messages.
|
||||
Connected,
|
||||
}
|
||||
|
||||
impl From<NotifyState> for JsonrpcNotifyState {
|
||||
fn from(state: NotifyState) -> Self {
|
||||
match state {
|
||||
NotifyState::NotConnected => Self::NotConnected,
|
||||
NotifyState::Connected => Self::Connected,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 verify the contact.
|
||||
/// Ask the user whether to start chatting with 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 is verified.
|
||||
/// Contact fingerprint matches.
|
||||
///
|
||||
/// Ask the user if they want to start chatting.
|
||||
FprOk {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use deltachat::contact::ContactId;
|
||||
use deltachat::reaction::Reactions;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
@@ -24,8 +23,11 @@ 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>,
|
||||
@@ -34,30 +36,24 @@ 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 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)
|
||||
}
|
||||
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();
|
||||
|
||||
JsonrpcReactions {
|
||||
reactions_by_contact,
|
||||
reactions: reactions_v,
|
||||
reactions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "ratelimit"
|
||||
version = "1.0.0"
|
||||
description = "Token bucket implementation"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use anyhow::{Result, bail, ensure};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
@@ -27,55 +27,6 @@ use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
}
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
@@ -228,7 +179,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::new(DC_MSG_ID_DAYMARKER) {
|
||||
if msg_id == MsgId::DAYMARKER {
|
||||
println!(
|
||||
"--------------------------------------------------------------------------------"
|
||||
);
|
||||
@@ -259,19 +210,13 @@ 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" }
|
||||
);
|
||||
|
||||
@@ -310,7 +255,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
export-keys\n\
|
||||
import-keys <key-file>\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
============================================="
|
||||
),
|
||||
@@ -450,15 +394,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
}
|
||||
"stop" => {
|
||||
context.stop_ongoing().await;
|
||||
}
|
||||
@@ -616,7 +551,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_ex(
|
||||
let msglist = chat::get_chat_msgs_ext(
|
||||
&context,
|
||||
sel_chat.get_id(),
|
||||
chat::MessageListOptions {
|
||||
@@ -630,7 +565,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::new(DC_MSG_ID_DAYMARKER),
|
||||
ChatItem::DayMarker { .. } => MsgId::DAYMARKER,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1119,11 +1054,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
|
||||
let mut res = format!(
|
||||
"Contact info for: {}:\nIcon: {}\n",
|
||||
name_n_addr,
|
||||
"Contact info for: {name} ({addr}):\nIcon: {}\n",
|
||||
match contact.get_profile_image(&context).await? {
|
||||
Some(image) => image.to_str().unwrap().to_string(),
|
||||
None => "NoIcon".to_string(),
|
||||
@@ -1206,12 +1141,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={width}, height={height}");
|
||||
} else {
|
||||
let Ok(buf) = read_file(&context, Path::new(arg1)).await else {
|
||||
bail!("Command failed.");
|
||||
}
|
||||
};
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={width}, height={height}");
|
||||
}
|
||||
"estimatedeletion" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
||||
|
||||
@@ -9,12 +9,12 @@ extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use anyhow::{Error, bail};
|
||||
use deltachat::EventType;
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::context::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::EventType;
|
||||
use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
@@ -147,7 +147,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
const IMEX_COMMANDS: [&str; 9] = [
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
@@ -156,7 +156,6 @@ const IMEX_COMMANDS: [&str; 10] = [
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
];
|
||||
|
||||
@@ -173,7 +172,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
const CHAT_COMMANDS: [&str; 38] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
@@ -182,7 +181,6 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"createchat",
|
||||
"creategroup",
|
||||
"createbroadcast",
|
||||
"createprotected",
|
||||
"addmember",
|
||||
"removemember",
|
||||
"groupname",
|
||||
@@ -266,10 +264,11 @@ impl Hinter for DcHelper {
|
||||
&CONTACT_COMMANDS[..],
|
||||
&MISC_COMMANDS[..],
|
||||
] {
|
||||
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos])) {
|
||||
if *entry != line && *entry != &line[..pos] {
|
||||
return Some(entry[pos..].to_owned());
|
||||
}
|
||||
if let Some(entry) = cmds.iter().find(|el| el.starts_with(&line[..pos]))
|
||||
&& *entry != line
|
||||
&& *entry != &line[..pos]
|
||||
{
|
||||
return Some(entry[pos..].to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -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.
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# You were 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).",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
@@ -35,6 +36,15 @@ 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.
|
||||
|
||||
@@ -133,10 +143,6 @@ 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."""
|
||||
@@ -144,9 +150,10 @@ class Account:
|
||||
return transports
|
||||
|
||||
def bring_online(self):
|
||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||
"""Start I/O, wait until all transports became IDLE and drop the events seen so far."""
|
||||
self.start_io()
|
||||
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
self._rpc.wait_for_all_work_done(self.id)
|
||||
self.clear_all_events()
|
||||
|
||||
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
@@ -186,7 +193,7 @@ class Account:
|
||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
"""Create a 1:1 chat with another account."""
|
||||
"""Create a single chat with another account."""
|
||||
return self.create_contact(account).create_chat()
|
||||
|
||||
def get_device_chat(self) -> Chat:
|
||||
@@ -222,7 +229,7 @@ class Account:
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
||||
"""Return 1:1 chat for a contact if it exists."""
|
||||
"""Return single chat for a contact if it exists."""
|
||||
if isinstance(contact, Contact):
|
||||
assert contact.account == self
|
||||
contact_id = contact.id
|
||||
@@ -265,7 +272,7 @@ class Account:
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
@property
|
||||
def device_contact(self) -> Chat:
|
||||
def device_contact(self) -> Contact:
|
||||
"""Account's device contact."""
|
||||
return Contact(self, SpecialContactId.DEVICE)
|
||||
|
||||
@@ -353,7 +360,7 @@ class Account:
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def secure_join(self, qrdata: str) -> Chat:
|
||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
|
||||
"""Continue the SecureJoin protocol started on another device.
|
||||
|
||||
The function returns immediately and the handshake runs in background, sending
|
||||
and receiving several messages.
|
||||
|
||||
@@ -252,7 +252,7 @@ class Chat:
|
||||
def get_contacts(self) -> list[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single/direct chats self-address is not included.
|
||||
For single chats self-address is not included.
|
||||
"""
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
@@ -70,6 +70,7 @@ 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"
|
||||
@@ -97,7 +98,7 @@ class ChatType(str, Enum):
|
||||
"""Chat type."""
|
||||
|
||||
SINGLE = "Single"
|
||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||
"""Single chat (a chat with a with a single contact)"""
|
||||
|
||||
GROUP = "Group"
|
||||
|
||||
@@ -246,13 +247,6 @@ class ProviderStatus(IntEnum):
|
||||
BROKEN = 3
|
||||
|
||||
|
||||
class PushNotifyState(IntEnum):
|
||||
"""Push notifications state."""
|
||||
|
||||
NOT_CONNECTED = 0
|
||||
CONNECTED = 2
|
||||
|
||||
|
||||
class ShowEmails(IntEnum):
|
||||
"""Show emails mode."""
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class Contact:
|
||||
return snapshot
|
||||
|
||||
def create_chat(self) -> "Chat":
|
||||
"""Create or get an existing 1:1 chat for this contact."""
|
||||
"""Create or get an existing single chat for this contact."""
|
||||
from .chat import Chat
|
||||
|
||||
return Chat(
|
||||
|
||||
@@ -48,6 +48,13 @@ 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()
|
||||
@@ -67,3 +74,7 @@ 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()
|
||||
|
||||
@@ -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) -> None:
|
||||
def send_webxdc_realtime_data(self, data):
|
||||
"""Send data to the realtime channel."""
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
|
||||
@@ -7,12 +7,13 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import random
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import AsyncGenerator, Optional
|
||||
import time
|
||||
import urllib.parse
|
||||
from typing import Iterator, Optional
|
||||
|
||||
import execnet
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||
@@ -26,19 +27,42 @@ 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()
|
||||
return f"deltachat-rpc-server: {fn} [{version}]"
|
||||
headers.append(f"RPC-SERVER: {fn} [{version}]")
|
||||
break
|
||||
|
||||
return None
|
||||
return headers
|
||||
|
||||
|
||||
class ACFactory:
|
||||
class RPCAccountFactory:
|
||||
"""Test account factory."""
|
||||
|
||||
def __init__(self, deltachat: DeltaChat) -> None:
|
||||
@@ -52,7 +76,7 @@ class ACFactory:
|
||||
"""Create a new unconfigured bot."""
|
||||
return Bot(self.get_unconfigured_account())
|
||||
|
||||
def get_credentials(self) -> (str, str):
|
||||
def get_credentials(self) -> tuple[str, str]:
|
||||
"""Generate new credentials for chatmail account."""
|
||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
@@ -104,7 +128,7 @@ class ACFactory:
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
|
||||
"""Create a new single chat between ac1 and ac2 accepted on both sides.
|
||||
|
||||
Returned chat is a chat with ac2 from ac1 point of view.
|
||||
"""
|
||||
@@ -151,7 +175,7 @@ class ACFactory:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc(tmp_path) -> AsyncGenerator:
|
||||
def rpc(tmp_path) -> Iterator[Rpc]:
|
||||
"""RPC client fixture."""
|
||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||
with rpc_server:
|
||||
@@ -165,20 +189,20 @@ def dc(rpc) -> DeltaChat:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(dc) -> AsyncGenerator:
|
||||
def acf(dc) -> RPCAccountFactory:
|
||||
"""Return account factory fixture."""
|
||||
return ACFactory(dc)
|
||||
return RPCAccountFactory(dc)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data():
|
||||
def rpcdata():
|
||||
"""Test data."""
|
||||
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join("test-data")
|
||||
if datadir.isdir():
|
||||
for path in pathlib.Path(__file__).parents:
|
||||
datadir = path / "test-data"
|
||||
if datadir.is_dir():
|
||||
self.path = datadir
|
||||
return
|
||||
raise Exception("Data path cannot be found")
|
||||
@@ -268,26 +292,33 @@ def get_core_python_env(tmp_path_factory):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
|
||||
def alice_and_remote_bob(tmp_path, acf, 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
|
||||
that can use the `bob` online account, and the `bob_contact_alice`.
|
||||
"""
|
||||
from execnet import makegateway
|
||||
|
||||
def factory(core_version):
|
||||
python, rpc_server_path = get_core_python_env(core_version)
|
||||
gw = execnet.makegateway(f"popen//python={python}")
|
||||
gw = makegateway(f"popen//python={python}")
|
||||
|
||||
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()
|
||||
dclogin_qr = f"dclogin://{urllib.parse.quote(addr, safe='@')}?p={urllib.parse.quote(password)}&v=1"
|
||||
if os.environ["CHATMAIL_DOMAIN"].startswith("_"):
|
||||
dclogin_qr += "&ic=3"
|
||||
|
||||
# trigger getting an online account on bob's side
|
||||
channel.send((accounts_dir, str(rpc_server_path), cm))
|
||||
channel.send((accounts_dir, str(rpc_server_path), dclogin_qr))
|
||||
|
||||
# meanwhile get a local alice account
|
||||
alice = acfactory.get_online_account()
|
||||
alice = acf.get_online_account()
|
||||
channel.send(alice.self_contact.make_vcard())
|
||||
|
||||
# wait for bob to have started
|
||||
@@ -316,10 +347,8 @@ def remote_bob_loop(channel):
|
||||
import os
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
|
||||
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
|
||||
accounts_dir, rpc_server_path, dclogin_qr = channel.receive()
|
||||
|
||||
# older core versions don't support specifying rpc_server_path
|
||||
# so we can't just pass `rpc_server_path` argument to Rpc constructor
|
||||
@@ -330,8 +359,13 @@ def remote_bob_loop(channel):
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
||||
acfactory = ACFactory(dc)
|
||||
bob = acfactory.get_online_account()
|
||||
|
||||
# RPCAccountFactory 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.bring_online()
|
||||
|
||||
alice_vcard = channel.receive()
|
||||
[alice_contact] = bob.import_vcard(alice_vcard)
|
||||
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
@@ -38,8 +39,15 @@ class RpcMethod:
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
self.rpc.request_results[request_id] = queue = Queue()
|
||||
self.rpc.request_queue.put(request)
|
||||
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)
|
||||
|
||||
def rpc_future():
|
||||
"""Wait for the request to receive a result."""
|
||||
@@ -78,6 +86,10 @@ 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
|
||||
@@ -107,6 +119,7 @@ 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()
|
||||
@@ -123,6 +136,8 @@ 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
|
||||
@@ -135,11 +150,31 @@ 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.process.stdin.close()
|
||||
self.reader_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.reader_thread.join()
|
||||
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()
|
||||
@@ -162,9 +197,11 @@ class Rpc:
|
||||
# Log an exception if the reader loop dies.
|
||||
logging.exception("Exception in the reader loop")
|
||||
finally:
|
||||
# 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"}})
|
||||
# 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)
|
||||
|
||||
def writer_loop(self) -> None:
|
||||
"""Writer loop ensuring only a single thread writes requests."""
|
||||
|
||||
@@ -22,8 +22,10 @@ ALL = "1:*"
|
||||
class DirectImap:
|
||||
"""Internal Python-level IMAP handling."""
|
||||
|
||||
def __init__(self, account: Account) -> None:
|
||||
def __init__(self, account: Account, addr=None, password=None) -> 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()
|
||||
@@ -33,11 +35,15 @@ class DirectImap:
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = 993
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
user = self.addr
|
||||
host = user.rsplit("@")[-1]
|
||||
pw = self.account.get_config("mail_pw")
|
||||
pw = self.password
|
||||
|
||||
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
|
||||
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_context)
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
@@ -165,7 +171,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)))]
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)), mark_seen=False)]
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
@@ -174,9 +180,6 @@ 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]:
|
||||
|
||||
@@ -5,16 +5,16 @@ from typing import TYPE_CHECKING
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
|
||||
|
||||
|
||||
def test_event_on_configuration(acfactory: ACFactory) -> None:
|
||||
def test_event_on_configuration(acf: RPCAccountFactory) -> None:
|
||||
"""
|
||||
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
||||
"""
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
addr, password = acf.get_credentials()
|
||||
account = acf.get_unconfigured_account()
|
||||
account.clear_all_events()
|
||||
assert not account.is_configured()
|
||||
future = account.add_or_update_transport.future({"addr": addr, "password": password})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from deltachat_rpc_client import EventType, Message
|
||||
|
||||
|
||||
def test_calls(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_calls(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
place_call_info = "offer"
|
||||
accept_call_info = "answer"
|
||||
@@ -35,14 +35,14 @@ def test_calls(acfactory) -> None:
|
||||
assert incoming_call_message.get_call_info().state.kind == "Completed"
|
||||
|
||||
|
||||
def test_video_call(acfactory) -> None:
|
||||
def test_video_call(acf) -> 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 = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.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(acfactory) -> None:
|
||||
assert incoming_call_message.get_call_info().has_video
|
||||
|
||||
|
||||
def test_audio_call(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_audio_call(acf) -> None:
|
||||
alice, bob = acf.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(acfactory) -> None:
|
||||
assert not incoming_call_message.get_call_info().has_video
|
||||
|
||||
|
||||
def test_ice_servers(acfactory) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
def test_ice_servers(acf) -> None:
|
||||
alice = acf.get_online_account()
|
||||
|
||||
ice_servers = alice.ice_servers()
|
||||
assert len(ice_servers) == 1
|
||||
|
||||
|
||||
def test_no_contact_request_call(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_no_contact_request_call(acf) -> None:
|
||||
alice, bob = acf.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(acfactory) -> None:
|
||||
break
|
||||
|
||||
|
||||
def test_who_can_call_me_nobody(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_who_can_call_me_nobody(acf) -> None:
|
||||
alice, bob = acf.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(acfactory) -> None:
|
||||
break
|
||||
|
||||
|
||||
def test_who_can_call_me_everybody(acfactory) -> None:
|
||||
def test_who_can_call_me_everybody(acf) -> None:
|
||||
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
# Bob sets "who can call me" to "nobody" (0)
|
||||
bob.set_config("who_can_call_me", "0")
|
||||
|
||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import Account, EventType, const
|
||||
from deltachat_rpc_client import EventType, const
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
|
||||
|
||||
|
||||
def wait_for_chatlist_and_specific_item(account, chat_id):
|
||||
@@ -40,11 +40,11 @@ def wait_for_chatlist(account):
|
||||
break
|
||||
|
||||
|
||||
def test_delivery_status(acfactory: ACFactory) -> None:
|
||||
def test_delivery_status(acf: RPCAccountFactory) -> None:
|
||||
"""
|
||||
Test change status on chatlistitem when status changes (delivered, read)
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.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(acfactory: ACFactory) -> None:
|
||||
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
|
||||
|
||||
|
||||
def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||
def test_delivery_status_failed(acf: RPCAccountFactory) -> None:
|
||||
"""
|
||||
Test change status on chatlistitem when status changes failed
|
||||
"""
|
||||
(alice,) = acfactory.get_online_accounts(1)
|
||||
(alice,) = acf.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(acfactory: ACFactory) -> None:
|
||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> 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 = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.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(acfactory: ACFactory) -> 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="../test-data/image/screenshot.jpg",
|
||||
file=rpcdata.get_path("image/screenshot.jpg"),
|
||||
)
|
||||
|
||||
message = alice.wait_for_incoming_msg()
|
||||
@@ -144,8 +144,8 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
|
||||
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
|
||||
alice, bob = acf.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(acfactory: ACFactory) -> [Account, Account, Acc
|
||||
return [alice, alice_second_device, bob, alice_chat_bob]
|
||||
|
||||
|
||||
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
def test_imap_sync_seen_msgs(acf: RPCAccountFactory) -> 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(acfactory)
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
|
||||
|
||||
bob.create_chat(alice)
|
||||
|
||||
@@ -191,11 +191,11 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
|
||||
|
||||
|
||||
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
|
||||
def test_multidevice_sync_chat(acf: RPCAccountFactory) -> 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(acfactory)
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acf)
|
||||
|
||||
alice_chat_bob.archive()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -16,7 +17,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(acfactory, alice_and_remote_bob, version) -> None:
|
||||
def test_qr_setup_contact(acf, 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)
|
||||
|
||||
@@ -24,28 +25,48 @@ def test_qr_setup_contact(acfactory, 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.is_verified
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
# 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")
|
||||
|
||||
# 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 = acfactory.get_online_account()
|
||||
alice2 = acf.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.20.0")
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
|
||||
|
||||
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
||||
|
||||
@@ -53,14 +74,149 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
assert msg.get_snapshot().text == "hello"
|
||||
|
||||
|
||||
def test_second_device(acfactory, alice_and_remote_bob) -> None:
|
||||
def test_second_device(acf, 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.20.0")
|
||||
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.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 = acfactory.get_unconfigured_account()
|
||||
new_account = acf.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"
|
||||
|
||||
@@ -5,12 +5,12 @@ from imap_tools import AND, U
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
def test_moved_markseen(acfactory, direct_imap, log):
|
||||
def test_moved_markseen(acf, direct_imap, log):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.get_online_account()
|
||||
ac1 = acf.get_online_account()
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
addr, password = acf.get_credentials()
|
||||
ac2 = acf.get_unconfigured_account()
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac2.bring_online()
|
||||
|
||||
@@ -57,24 +57,28 @@ def test_moved_markseen(acfactory, 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(acfactory, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
def test_markseen_message_and_mdn(acf, direct_imap):
|
||||
ac1, ac2 = acf.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")
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
acf.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:
|
||||
while True:
|
||||
uids = set()
|
||||
while len(uids) < 2:
|
||||
event = ac.wait_for_event()
|
||||
if event.kind == EventType.INFO and rex.search(event.msg):
|
||||
break
|
||||
if event.kind == EventType.INFO and (match := rex.search(event.msg)):
|
||||
uids.update(re.split("[,:]", match.group(1)))
|
||||
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
@@ -87,8 +91,8 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
||||
|
||||
|
||||
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
def test_trash_multiple_messages(acf, direct_imap, log):
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
@@ -97,7 +101,7 @@ def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
ac2.set_config("sync_msgs", "0")
|
||||
|
||||
ac2.start_io()
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat12 = acf.get_accepted_chat(ac1, ac2)
|
||||
|
||||
log.section("ac1: sending 3 messages")
|
||||
texts = ["first", "second", "third"]
|
||||
|
||||
@@ -7,15 +7,25 @@ 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
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
|
||||
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
|
||||
def path_to_webxdc(request):
|
||||
@@ -35,7 +45,11 @@ def log(msg):
|
||||
logging.info(msg)
|
||||
|
||||
|
||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
|
||||
# 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):
|
||||
assert ac1.get_config("webxdc_realtime_enabled") == "1"
|
||||
assert ac2.get_config("webxdc_realtime_enabled") == "1"
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
@@ -52,45 +66,44 @@ def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
@contextmanager
|
||||
def send_realtime_data_forever(msgs, data=None):
|
||||
stop = threading.Event()
|
||||
data = data or [SETUP_DATA] * len(msgs)
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
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()
|
||||
|
||||
|
||||
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 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 test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
def test_realtime_sequentially(acf, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection sequentially."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||
ac1_webxdc_msg = acf.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"
|
||||
@@ -98,7 +111,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
# send iroh announcements sequentially
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||
acf.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()
|
||||
@@ -106,7 +119,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||
acf.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()
|
||||
@@ -117,50 +130,27 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
data = os.urandom(128000)
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(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
|
||||
assert ac2.wait_for_realtime_data(ac2_webxdc_msg.id) == data
|
||||
|
||||
|
||||
def test_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
def test_realtime_simultaneously(acf, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
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])])
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
|
||||
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
def test_two_parallel_realtime_simultaneously(acf, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
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])])
|
||||
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)])
|
||||
|
||||
|
||||
def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
def test_no_duplicate_messages(acf, path_to_webxdc):
|
||||
"""Test that messages are received only once."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
|
||||
@@ -174,50 +164,29 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
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
|
||||
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 test_no_reordering(acfactory, path_to_webxdc):
|
||||
def test_no_reordering(acf, path_to_webxdc):
|
||||
"""Test that sending a lot of realtime messages does not result in reordering."""
|
||||
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")])
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True)
|
||||
|
||||
for i in range(200):
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data([i])
|
||||
|
||||
for i in range(200):
|
||||
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")
|
||||
# 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"
|
||||
|
||||
|
||||
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
def test_advertisement_after_chatting(acf, path_to_webxdc):
|
||||
"""Test that realtime advertisement is assigned to the correct message after chatting."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
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()
|
||||
@@ -236,17 +205,14 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
|
||||
|
||||
def test_realtime_large_webxdc(acfactory, path_to_large_webxdc):
|
||||
def test_realtime_large_webxdc(acf, 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 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
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)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
def test_set_location(dc, acfactory) -> None:
|
||||
def test_set_location(dc, acf) -> 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.
|
||||
acfactory.new_configured_account()
|
||||
acf.new_configured_account()
|
||||
assert not dc.set_location(3.0, 4.0, 0.1)
|
||||
|
||||
|
||||
def test_send_locations_to_chat(dc, acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_send_locations_to_chat(dc, acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
assert not alice.is_sending_locations()
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
|
||||
@@ -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(acfactory):
|
||||
ac = acfactory.get_online_account()
|
||||
def test_bcc_self_is_enabled_when_setting_up_second_device(acf):
|
||||
ac = acf.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(acfactory):
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
def test_one_account_send_bcc_setting(acf, log, direct_imap):
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
@@ -75,9 +75,9 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_multidevice_sync_seen(acfactory, log):
|
||||
def test_multidevice_sync_seen(acf, log):
|
||||
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
@@ -127,3 +127,34 @@ def test_multidevice_sync_seen(acfactory, 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
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
@@ -5,11 +8,27 @@ from deltachat_rpc_client.const import ChatType, DownloadState
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_add_second_address(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
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()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
qr = acf.get_account_qr()
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
@@ -18,20 +37,24 @@ def test_add_second_address(acfactory) -> None:
|
||||
|
||||
first_addr = account.list_transports()[0]["addr"]
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
third_addr = account.list_transports()[2]["addr"]
|
||||
|
||||
# Cannot delete the first address.
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.delete_transport(first_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
|
||||
|
||||
account.delete_transport(second_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.delete_transport(third_addr)
|
||||
|
||||
|
||||
def test_change_address(acfactory) -> None:
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_change_address(acf) -> None:
|
||||
"""Test Alice configuring a second transport and removing the first one."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
@@ -41,28 +64,18 @@ def test_change_address(acfactory) -> None:
|
||||
sender_addr1 = msg1.sender.get_snapshot().address
|
||||
|
||||
alice.stop_io()
|
||||
old_alice_addr = alice.get_config("configured_addr")
|
||||
old_alice_addr = alice.list_transports()[0]["addr"]
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr in alice_vcard
|
||||
qr = acfactory.get_account_qr()
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
with pytest.raises(JsonRpcError):
|
||||
# Cannot use the address that is not
|
||||
# configured for any transport.
|
||||
alice.set_config("configured_addr", bob_addr)
|
||||
|
||||
# Load old address so it is cached.
|
||||
assert alice.get_config("configured_addr") == old_alice_addr
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
# Make sure that setting `configured_addr` invalidated the cache.
|
||||
assert alice.get_config("configured_addr") == new_alice_addr
|
||||
alice.delete_transport(old_alice_addr)
|
||||
|
||||
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!")
|
||||
@@ -76,18 +89,37 @@ def test_change_address(acfactory) -> None:
|
||||
assert sender_addr2 == new_alice_addr
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
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)
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
alice.stop_io()
|
||||
qr = acfactory.get_account_qr()
|
||||
qr = acf.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="../test-data/image/screenshot.jpg")
|
||||
chat_bob_alice.send_message(file=rpcdata.get_path("image/screenshot.jpg"))
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
snapshot = msg.get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
@@ -103,15 +135,15 @@ def test_download_on_demand(acfactory) -> None:
|
||||
assert msg.get_snapshot().download_state == dstate
|
||||
|
||||
|
||||
def test_reconfigure_transport(acfactory) -> None:
|
||||
def test_reconfigure_transport(acf) -> None:
|
||||
"""Test that reconfiguring the transport works."""
|
||||
account = acfactory.get_online_account()
|
||||
account = acf.get_online_account()
|
||||
|
||||
[transport] = account.list_transports()
|
||||
account.add_or_update_transport(transport)
|
||||
|
||||
|
||||
def test_transport_synchronization(acfactory, log) -> None:
|
||||
def test_transport_synchronization(acf, log) -> None:
|
||||
"""Test synchronization of transports between devices."""
|
||||
|
||||
def wait_for_io_started(ac):
|
||||
@@ -120,22 +152,24 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
if "scheduler is running" in ev.msg:
|
||||
return
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
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_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
qr = acf.get_account_qr()
|
||||
|
||||
ac1.add_transport_from_qr(qr)
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_transports(ac1_clone, 2)
|
||||
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)
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_transports(ac1, 3)
|
||||
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")
|
||||
@@ -143,24 +177,17 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
addr3 = transport3["addr"]
|
||||
ac1_clone.delete_transport(transport2["addr"])
|
||||
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_transports(ac1, 2)
|
||||
wait_for_io_started(ac1)
|
||||
[transport1, transport3] = ac1.list_transports()
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
log.section("ac1 changes the sending 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"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_transports(ac1_clone, 1)
|
||||
wait_for_io_started(ac1_clone)
|
||||
[transport3] = ac1_clone.list_transports()
|
||||
assert transport3["addr"] == addr3
|
||||
@@ -173,15 +200,16 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
qr = acf.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
|
||||
@@ -192,10 +220,7 @@ def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
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")
|
||||
log.section("ac1_clone receives a message via the new transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
ac1_chat.send_text("Hello!")
|
||||
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
||||
@@ -205,12 +230,12 @@ def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
||||
|
||||
|
||||
def test_recognize_self_address(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_recognize_self_address(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
@@ -221,10 +246,10 @@ def test_recognize_self_address(acfactory) -> None:
|
||||
assert msg.chat == alice.create_chat(bob)
|
||||
|
||||
|
||||
def test_transport_limit(acfactory) -> None:
|
||||
def test_transport_limit(acf) -> None:
|
||||
"""Test transports limit."""
|
||||
account = acfactory.get_online_account()
|
||||
qr = acfactory.get_account_qr()
|
||||
account = acf.get_online_account()
|
||||
qr = acf.get_account_qr()
|
||||
|
||||
limit = 5
|
||||
|
||||
@@ -237,33 +262,22 @@ def test_transport_limit(acfactory) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
third_addr = account.list_transports()[2]["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.delete_transport(second_addr)
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory) -> None:
|
||||
def test_message_info_imap_urls(acf) -> None:
|
||||
"""Test that message info contains IMAP URLs of where the message was received."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
for i in range(3):
|
||||
qr = acf.get_account_qr()
|
||||
for _ in range(3):
|
||||
alice.add_transport_from_qr(qr)
|
||||
# Wait for all transports to go IDLE after adding each one.
|
||||
for _ in range(i + 1):
|
||||
alice.bring_online()
|
||||
alice.bring_online()
|
||||
|
||||
# Enable multi-device mode so messages are not deleted immediately.
|
||||
alice.set_config("bcc_self", "1")
|
||||
@@ -293,20 +307,12 @@ def test_message_info_imap_urls(acfactory) -> None:
|
||||
assert f"{new_alice_addr}/INBOX" in msg_info
|
||||
|
||||
|
||||
def test_remove_primary_transport(acfactory, log) -> None:
|
||||
def test_remove_primary_transport(acf, log) -> None:
|
||||
"""Test that after removing the primary relay, Alice can still receive messages."""
|
||||
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)
|
||||
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
|
||||
|
||||
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()
|
||||
@@ -314,6 +320,7 @@ def test_remove_primary_transport(acfactory, 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()
|
||||
|
||||
@@ -321,4 +328,75 @@ def test_remove_primary_transport(acfactory, 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.create_chat(bob)
|
||||
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
|
||||
|
||||
|
||||
def test_multitransport_mdn(acf):
|
||||
"""Test sending an MDN right after configuring two transports."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
alice.create_chat(bob)
|
||||
bob_msg = bob.create_chat(alice).send_text("Hello!")
|
||||
alice.wait_for_incoming_msg().mark_seen()
|
||||
assert bob.wait_for_event(EventType.MSG_READ).msg_id == bob_msg.id
|
||||
|
||||
@@ -4,48 +4,29 @@ 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(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_qr_setup_contact(acf) -> None:
|
||||
alice, bob = acf.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.is_verified
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
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.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
|
||||
assert bob_contact_alice_snapshot.e2ee_avail
|
||||
|
||||
|
||||
def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
def test_qr_setup_contact_svg(acf) -> None:
|
||||
alice = acf.new_configured_account()
|
||||
_, _, domain = alice.get_config("addr").rpartition("@")
|
||||
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
@@ -59,8 +40,8 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
assert "Alice" in svg
|
||||
|
||||
|
||||
def test_qr_securejoin(acfactory):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
def test_qr_securejoin(acf):
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
|
||||
# Setup second device for Alice
|
||||
# to test observing securejoin protocol.
|
||||
@@ -81,26 +62,24 @@ def test_qr_securejoin(acfactory):
|
||||
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.is_verified
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "You were 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.is_verified
|
||||
assert bob_contact_alice_snapshot.e2ee_avail
|
||||
|
||||
# Start second Alice device.
|
||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||
# Alice observes the securejoin protocol on the 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.is_verified
|
||||
assert alice2_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||
logging.info("Fiona joins the group via alice2")
|
||||
@@ -111,8 +90,8 @@ def test_qr_securejoin(acfactory):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
|
||||
alice2 = alice.clone()
|
||||
bob2 = bob.clone()
|
||||
@@ -151,9 +130,9 @@ def test_qr_securejoin_broadcast(acfactory, 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 is verified.
|
||||
# Check that the chat partner's key is known.
|
||||
contact_snapshot = contact.get_snapshot()
|
||||
assert contact_snapshot.is_verified
|
||||
assert contact_snapshot.e2ee_avail
|
||||
|
||||
chat = get_broadcast(ac)
|
||||
chat_msgs = chat.get_messages()
|
||||
@@ -252,9 +231,9 @@ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
||||
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||
|
||||
|
||||
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
def test_qr_securejoin_contact_request(acf) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
@@ -278,8 +257,8 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||
|
||||
|
||||
def test_qr_readreceipt(acfactory) -> None:
|
||||
alice, bob, charlie = acfactory.get_online_accounts(3)
|
||||
def test_qr_readreceipt(acf) -> None:
|
||||
alice, bob, charlie = acf.get_online_accounts(3)
|
||||
|
||||
logging.info("Bob and Charlie setup contact with Alice")
|
||||
qr_code = alice.get_qr_code()
|
||||
@@ -335,24 +314,24 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
|
||||
def test_setup_contact_resetup(acfactory) -> None:
|
||||
def test_setup_contact_resetup(acf) -> None:
|
||||
"""Tests that setup contact works after Alice resets the device and changes the key."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
alice = acfactory.resetup_account(alice)
|
||||
alice = acf.resetup_account(alice)
|
||||
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
logging.info("ac1 creates a group")
|
||||
chat = ac1.create_group("Group")
|
||||
@@ -362,11 +341,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# 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")
|
||||
logging.info("ac3 joins the group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
@@ -374,9 +349,9 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac3_contact_ac2_old = ac3.create_contact(ac2)
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
ac2 = acf.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
logging.info("ac2 scans ac3's QR code again")
|
||||
qr_code = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
@@ -416,28 +391,20 @@ def test_verified_group_member_added_recovery(acfactory) -> 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(acfactory):
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
"""Regression test for
|
||||
issue <https://github.com/chatmail/core/issues/4894>.
|
||||
"""
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
ac1, ac2, ac3, ac4 = acf.get_online_accounts(4)
|
||||
|
||||
logging.info("ac3: verify with ac2")
|
||||
logging.info("ac3: set up contact 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 verified group
|
||||
# we first create a fully joined verified group, and then start
|
||||
# in order for ac2 to have pending bobstate with a group
|
||||
# we first create a fully joined 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")
|
||||
@@ -446,7 +413,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||
# ensure ac1 can write and ac2 receives messages in the chat
|
||||
ch1.send_text("ac1 says hello")
|
||||
while 1:
|
||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
@@ -459,11 +426,11 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
ac1.remove()
|
||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# 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
|
||||
# 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
|
||||
|
||||
logging.info("ac3: create a verified group VG with ac2")
|
||||
logging.info("ac3: create a group VG with ac2")
|
||||
vg = ac3.create_group("ac3-created")
|
||||
vg.add_contact(ac3.create_contact(ac2))
|
||||
|
||||
@@ -484,17 +451,17 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
return
|
||||
|
||||
|
||||
def test_qr_new_group_unblocked(acfactory):
|
||||
def test_qr_new_group_unblocked(acf):
|
||||
"""Regression test for a bug introduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||
ac2 scans a 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.
|
||||
ac2 should receive a message and create a contact request for the group.
|
||||
Due to a bug previously ac2 created a blocked group.
|
||||
"""
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group("Group for joining")
|
||||
qr_code = ac1_chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
@@ -513,13 +480,13 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="AEAP is disabled for now")
|
||||
def test_aeap_flow_verified(acfactory):
|
||||
def test_aeap_flow(acf):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
addr, password = acf.get_credentials()
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
logging.info("ac1: create 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")
|
||||
@@ -555,65 +522,15 @@ def test_aeap_flow_verified(acfactory):
|
||||
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
|
||||
|
||||
|
||||
def test_gossip_verification(acfactory) -> None:
|
||||
alice, bob, carol = acfactory.get_online_accounts(3)
|
||||
|
||||
# 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:
|
||||
def test_securejoin_after_contact_resetup(acf) -> 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.
|
||||
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 = acfactory.get_online_accounts(3)
|
||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
||||
|
||||
# ac3 creates protected group with ac1.
|
||||
# ac3 creates a group with ac1.
|
||||
ac3_chat = ac3.create_group("Group")
|
||||
|
||||
# ac1 joins ac3 group.
|
||||
@@ -623,31 +540,27 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 verifies ac1
|
||||
# ac2 sets up contact with 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().is_verified
|
||||
assert ac2_contact_ac1.get_snapshot().e2ee_avail
|
||||
|
||||
# ac1 resetups the account.
|
||||
ac1 = acfactory.resetup_account(ac1)
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
ac1 = acf.resetup_account(ac1)
|
||||
|
||||
# ac1 goes offline.
|
||||
ac1.remove()
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
|
||||
ac2.secure_join(ac1_qr_code)
|
||||
|
||||
@@ -664,16 +577,13 @@ def test_securejoin_after_contact_resetup(acfactory) -> 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(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_withdraw_securejoin_qr(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Bob joins verified group")
|
||||
logging.info("Bob joins the group")
|
||||
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
@@ -682,7 +592,7 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
alice.clear_all_events()
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "You were 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()
|
||||
@@ -706,8 +616,8 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
break
|
||||
|
||||
|
||||
def test_qr_scan_updates_new_relay_address(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_qr_scan_updates_new_relay_address(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_alice_chat = bob.secure_join(alice.get_qr_code())
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
@@ -715,7 +625,7 @@ def test_qr_scan_updates_new_relay_address(acfactory):
|
||||
|
||||
for ac in [alice, bob]:
|
||||
old_addr = ac.get_config("configured_addr")
|
||||
ac.add_transport_from_qr(acfactory.get_account_qr())
|
||||
ac.add_transport_from_qr(acf.get_account_qr())
|
||||
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
|
||||
ac.delete_transport(old_addr)
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ def test_email_address_validity(rpc) -> None:
|
||||
assert not rpc.check_email_validity(addr)
|
||||
|
||||
|
||||
def test_acfactory(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
def test_acf(acf) -> None:
|
||||
account = acf.new_configured_account()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CONFIGURE_PROGRESS:
|
||||
@@ -61,9 +61,9 @@ def test_acfactory(acfactory) -> None:
|
||||
logging.info("Successful configuration")
|
||||
|
||||
|
||||
def test_configure_starttls(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
def test_configure_starttls(acf) -> None:
|
||||
addr, password = acf.get_credentials()
|
||||
account = acf.get_unconfigured_account()
|
||||
account.add_or_update_transport(
|
||||
{
|
||||
"addr": addr,
|
||||
@@ -75,10 +75,10 @@ def test_configure_starttls(acfactory) -> None:
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_lowercase_address(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
def test_lowercase_address(acf) -> None:
|
||||
addr, password = acf.get_credentials()
|
||||
addr_upper = addr.upper()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account = acf.get_unconfigured_account()
|
||||
account.add_or_update_transport(
|
||||
{
|
||||
"addr": addr_upper,
|
||||
@@ -103,9 +103,9 @@ def test_lowercase_address(acfactory) -> None:
|
||||
assert addr_upper not in param
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
def test_configure_ip(acf) -> None:
|
||||
addr, password = acf.get_credentials()
|
||||
account = acf.get_unconfigured_account()
|
||||
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
@@ -119,10 +119,10 @@ def test_configure_ip(acfactory) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_configure_alternative_port(acfactory) -> None:
|
||||
def test_configure_alternative_port(acf) -> None:
|
||||
"""Test that configuration with alternative port 443 works."""
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
addr, password = acf.get_credentials()
|
||||
account = acf.get_unconfigured_account()
|
||||
account.add_or_update_transport(
|
||||
{
|
||||
"addr": addr,
|
||||
@@ -134,9 +134,9 @@ def test_configure_alternative_port(acfactory) -> None:
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_list_transports(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
def test_list_transports(acf) -> None:
|
||||
addr, password = acf.get_credentials()
|
||||
account = acf.get_unconfigured_account()
|
||||
account.add_or_update_transport(
|
||||
{
|
||||
"addr": addr,
|
||||
@@ -152,8 +152,8 @@ def test_list_transports(acfactory) -> None:
|
||||
assert params["imapUser"] == addr
|
||||
|
||||
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_account(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
@@ -221,8 +221,8 @@ def test_account(acfactory) -> None:
|
||||
alice.stop_io()
|
||||
|
||||
|
||||
def test_mark_fresh_vs_self_mdn(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_mark_fresh_vs_self_mdn(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
bob.set_config("bcc_self", "1")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
@@ -245,8 +245,8 @@ def test_mark_fresh_vs_self_mdn(acfactory) -> None:
|
||||
assert bob_chat.get_fresh_message_count() == 2
|
||||
|
||||
|
||||
def test_chat(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_chat(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
@@ -275,7 +275,7 @@ def test_chat(acfactory) -> None:
|
||||
bob_chat_alice.unpin()
|
||||
bob_chat_alice.archive()
|
||||
bob_chat_alice.unarchive()
|
||||
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||
with pytest.raises(JsonRpcError): # can't set name for single chats
|
||||
bob_chat_alice.set_name("test")
|
||||
bob_chat_alice.set_ephemeral_timer(300)
|
||||
bob_chat_alice.get_encryption_info()
|
||||
@@ -315,8 +315,8 @@ def test_chat(acfactory) -> None:
|
||||
group.get_locations()
|
||||
|
||||
|
||||
def test_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_contact(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
@@ -332,8 +332,8 @@ def test_contact(acfactory) -> None:
|
||||
alice_contact_bob.create_chat()
|
||||
|
||||
|
||||
def test_message(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_message(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
@@ -363,8 +363,8 @@ def test_message(acfactory) -> None:
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
def test_receive_imf_failure(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_receive_imf_failure(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
@@ -392,8 +392,8 @@ def test_receive_imf_failure(acfactory) -> None:
|
||||
assert snapshot.error is None
|
||||
|
||||
|
||||
def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
def test_selfavatar_sync(acf, rpcdata, log) -> None:
|
||||
alice = acf.get_online_account()
|
||||
|
||||
log.section("Alice adds a second device")
|
||||
alice2 = alice.clone()
|
||||
@@ -402,7 +402,7 @@ def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
alice2.start_io()
|
||||
|
||||
log.section("First device changes avatar")
|
||||
image = data.get_path("image/avatar1000x1000.jpg")
|
||||
image = rpcdata.get_path("image/avatar1000x1000.jpg")
|
||||
alice.set_config("selfavatar", image)
|
||||
avatar_config = alice.get_config("selfavatar")
|
||||
avatar_hash = os.path.basename(avatar_config)
|
||||
@@ -417,11 +417,10 @@ def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
assert avatar_config != avatar_config2
|
||||
|
||||
|
||||
def test_dont_move_sync_msgs(acfactory, direct_imap):
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
def test_dont_move_sync_msgs(acf, direct_imap):
|
||||
addr, password = acf.get_credentials()
|
||||
ac1 = acf.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)
|
||||
@@ -448,8 +447,8 @@ def test_dont_move_sync_msgs(acfactory, direct_imap):
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_reaction_seen_on_another_dev(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.start_io()
|
||||
|
||||
@@ -474,8 +473,8 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
assert chat_id == alice2_chat_bob.id
|
||||
|
||||
|
||||
def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_2nd_device_events_when_msgs_are_seen(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.start_io()
|
||||
|
||||
@@ -503,9 +502,9 @@ def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
|
||||
assert chat_alice2.get_fresh_message_count() == 0
|
||||
|
||||
|
||||
def test_is_bot(acfactory) -> None:
|
||||
def test_is_bot(acf) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
@@ -519,18 +518,18 @@ def test_is_bot(acfactory) -> None:
|
||||
assert snapshot.is_bot
|
||||
|
||||
|
||||
def test_bot(acfactory) -> None:
|
||||
def test_bot(acf) -> None:
|
||||
mock = MagicMock()
|
||||
user = (acfactory.get_online_accounts(1))[0]
|
||||
bot = acfactory.new_configured_bot()
|
||||
bot2 = acfactory.new_configured_bot()
|
||||
user = (acf.get_online_accounts(1))[0]
|
||||
bot = acf.new_configured_bot()
|
||||
bot2 = acf.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 = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||
event = acf.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)
|
||||
@@ -543,28 +542,28 @@ def test_bot(acfactory) -> None:
|
||||
hook = track, events.NewMessage(r"hello")
|
||||
bot.add_hook(*hook)
|
||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = acf.process_message(from_account=user, to_client=bot, text="hello")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
event = acf.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
||||
acf.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
||||
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
|
||||
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
acf.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
assert len(mock.hook.mock_calls) == 2
|
||||
bot.remove_hook(*hook)
|
||||
|
||||
mock.hook.reset_mock()
|
||||
acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
acf.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = acf.process_message(from_account=user, to_client=bot, text="/help")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
|
||||
|
||||
def test_wait_next_messages(acfactory) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
def test_wait_next_messages(acf) -> None:
|
||||
alice = acf.get_online_account()
|
||||
|
||||
# Create a bot account so it does not receive device messages in the beginning.
|
||||
addr, password = acfactory.get_credentials()
|
||||
bot = acfactory.get_unconfigured_account()
|
||||
addr, password = acf.get_credentials()
|
||||
bot = acf.get_unconfigured_account()
|
||||
bot.set_config("bot", "1")
|
||||
bot.add_or_update_transport({"addr": addr, "password": password})
|
||||
assert bot.is_configured()
|
||||
@@ -590,19 +589,19 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
def test_import_export_backup(acf, tmp_path) -> None:
|
||||
alice = acf.new_configured_account()
|
||||
alice.export_backup(tmp_path)
|
||||
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2 = acf.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
|
||||
assert alice2.manager.get_system_info()
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
|
||||
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||
def test_import_export_online_all(acf, tmp_path, rpcdata, log) -> None:
|
||||
(ac1, some1) = acf.get_online_accounts(2)
|
||||
|
||||
log.section("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
@@ -610,7 +609,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
|
||||
original_image_path = data.get_path("image/avatar64x64.png")
|
||||
original_image_path = rpcdata.get_path("image/avatar64x64.png")
|
||||
chat1.send_file(str(original_image_path))
|
||||
|
||||
# Add another 100KB file that ensures that the progress is smooth enough
|
||||
@@ -661,7 +660,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
|
||||
ac1.start_io()
|
||||
|
||||
log.section("get fresh empty account")
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2 = acf.get_unconfigured_account()
|
||||
|
||||
log.section("import backup and check it's proper")
|
||||
ac2.import_backup(files_written[0])
|
||||
@@ -698,8 +697,8 @@ def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
|
||||
assert len(list(backupdir.glob("*.tar"))) == 2
|
||||
|
||||
|
||||
def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_import_export_keys(acf, tmp_path) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
@@ -711,7 +710,7 @@ def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||
alice_keys_path = tmp_path / "alice_keys"
|
||||
alice_keys_path.mkdir()
|
||||
alice.export_self_keys(alice_keys_path)
|
||||
alice = acfactory.resetup_account(alice)
|
||||
alice = acf.resetup_account(alice)
|
||||
alice.import_self_keys(alice_keys_path)
|
||||
|
||||
snapshot.chat.accept()
|
||||
@@ -746,9 +745,14 @@ 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(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
def test_mdn_doesnt_break_autocrypt(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
@@ -778,10 +782,10 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n_accounts", [3, 2])
|
||||
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
def test_download_limit_chat_assignment(acf, tmp_path, n_accounts):
|
||||
download_limit = 300000
|
||||
|
||||
alice, *others = acfactory.get_online_accounts(n_accounts)
|
||||
alice, *others = acf.get_online_accounts(n_accounts)
|
||||
bob = others[0]
|
||||
|
||||
alice_group = alice.create_group("test group")
|
||||
@@ -817,10 +821,10 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
assert snapshot.chat == bob_group
|
||||
|
||||
|
||||
def test_download_small_msg_first(acfactory, tmp_path):
|
||||
def test_download_small_msg_first(acf, tmp_path):
|
||||
download_limit = 70000
|
||||
|
||||
alice, bob0 = acfactory.get_online_accounts(2)
|
||||
alice, bob0 = acf.get_online_accounts(2)
|
||||
bob1 = bob0.clone()
|
||||
bob1.set_config("download_limit", str(download_limit))
|
||||
|
||||
@@ -841,14 +845,14 @@ def test_download_small_msg_first(acfactory, tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("delete_chat", [False, True])
|
||||
def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
|
||||
def test_delete_available_msg(acf, 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 = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
bob.set_config("download_limit", str(download_limit))
|
||||
# Avoid immediate deletion from the server
|
||||
alice.set_config("bcc_self", "1")
|
||||
@@ -891,8 +895,8 @@ def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
|
||||
break
|
||||
|
||||
|
||||
def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_delete_fully_downloaded_msg(acf, tmp_path, direct_imap):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
# Avoid immediate deletion from the server
|
||||
bob.set_config("bcc_self", "1")
|
||||
|
||||
@@ -927,8 +931,8 @@ def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
|
||||
break
|
||||
|
||||
|
||||
def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_imap_autodelete_fully_downloaded_msg(acf, tmp_path, direct_imap):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
chat_alice = alice.create_chat(bob)
|
||||
path = tmp_path / "large"
|
||||
@@ -956,12 +960,12 @@ def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
|
||||
break
|
||||
|
||||
|
||||
def test_markseen_contact_request(acfactory):
|
||||
def test_markseen_contact_request(acf):
|
||||
"""
|
||||
Test that seen status is synchronized for contact request messages
|
||||
even though read receipt is not sent.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
# Bob sets up a second device.
|
||||
bob2 = bob.clone()
|
||||
@@ -980,11 +984,11 @@ def test_markseen_contact_request(acfactory):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("team_profile", [True, False])
|
||||
def test_no_markseen_in_team_profile(team_profile, acfactory):
|
||||
def test_no_markseen_in_team_profile(team_profile, acf):
|
||||
"""
|
||||
Test that seen status is synchronized iff `team_profile` isn't set.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
if team_profile:
|
||||
bob.set_config("team_profile", "1")
|
||||
|
||||
@@ -1003,6 +1007,11 @@ def test_no_markseen_in_team_profile(team_profile, acfactory):
|
||||
|
||||
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
|
||||
@@ -1020,11 +1029,11 @@ def test_no_markseen_in_team_profile(team_profile, acfactory):
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
def test_read_receipt(acfactory):
|
||||
def test_read_receipt(acf):
|
||||
"""
|
||||
Test sending a read receipt and ensure it is attributed to the correct contact.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
@@ -1043,15 +1052,15 @@ def test_read_receipt(acfactory):
|
||||
assert read_receipt_cnt == 1
|
||||
|
||||
|
||||
def test_get_http_response(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
def test_get_http_response(acf):
|
||||
alice = acf.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(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
def test_configured_imap_certificate_checks(acf):
|
||||
alice = acf.new_configured_account()
|
||||
|
||||
# Certificate checks should be configured (not None)
|
||||
assert "cert_strict" in alice.get_info().used_transport_settings
|
||||
@@ -1070,8 +1079,8 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
def test_no_old_msg_is_fresh(acf):
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.start_io()
|
||||
|
||||
@@ -1098,9 +1107,9 @@ def test_no_old_msg_is_fresh(acfactory):
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
|
||||
def test_rename_synchronization(acfactory):
|
||||
def test_rename_synchronization(acf):
|
||||
"""Test synchronization of contact renaming."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.bring_online()
|
||||
|
||||
@@ -1115,9 +1124,9 @@ def test_rename_synchronization(acfactory):
|
||||
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
|
||||
|
||||
|
||||
def test_rename_group(acfactory):
|
||||
def test_rename_group(acf):
|
||||
"""Test renaming the group."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_group = alice.create_group("Test group")
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
@@ -1146,8 +1155,8 @@ def test_get_all_accounts_deadlock(rpc):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||
def test_leave_broadcast(acfactory, all_devices_online):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_leave_broadcast(acf, all_devices_online):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob2 = bob.clone()
|
||||
|
||||
@@ -1247,8 +1256,8 @@ def test_leave_broadcast(acfactory, all_devices_online):
|
||||
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
||||
|
||||
|
||||
def test_leave_and_delete_group(acfactory, log):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_leave_and_delete_group(acf, log):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
log.section("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
@@ -1271,12 +1280,12 @@ def test_leave_and_delete_group(acfactory, log):
|
||||
alice.wait_for_event(EventType.CHAT_MODIFIED)
|
||||
|
||||
|
||||
def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||
def test_immediate_autodelete(acf, direct_imap, log):
|
||||
"""
|
||||
`bcc_self` is off by default,
|
||||
so that messages are supposed to be immediately autodeleted
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
log.section("ac1: create chat with ac2")
|
||||
@@ -1307,8 +1316,8 @@ def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||
assert ev.msg_id == sent_msg.id
|
||||
|
||||
|
||||
def test_background_fetch(acfactory, dc):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
def test_background_fetch(acf, dc):
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1.stop_io()
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
@@ -1344,8 +1353,24 @@ def test_background_fetch(acfactory, dc):
|
||||
break
|
||||
|
||||
|
||||
def test_message_exists(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
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)
|
||||
chat = ac1.create_chat(ac2)
|
||||
message1 = chat.send_text("Hello!")
|
||||
message2 = chat.send_text("Hello again!")
|
||||
@@ -1363,7 +1388,7 @@ def test_message_exists(acfactory):
|
||||
assert not message2.exists()
|
||||
|
||||
|
||||
def test_synchronize_member_list_on_group_rejoin(acfactory, log):
|
||||
def test_synchronize_member_list_on_group_rejoin(acf, 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
|
||||
@@ -1371,7 +1396,7 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, 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 = acfactory.get_online_accounts(3)
|
||||
ac1, ac2, ac3 = accounts = acf.get_online_accounts(3)
|
||||
|
||||
log.section("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group("title1")
|
||||
@@ -1407,17 +1432,17 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, log):
|
||||
assert msg.get_snapshot().chat.num_contacts() == 2
|
||||
|
||||
|
||||
def test_large_message(acfactory) -> None:
|
||||
def test_large_message(acf, rpcdata) -> None:
|
||||
"""
|
||||
Test sending large message without download limit set,
|
||||
so it is sent with pre-message but downloaded without user interaction.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob = acf.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="../test-data/image/screenshot.jpg",
|
||||
file=rpcdata.get_path("image/screenshot.jpg"),
|
||||
)
|
||||
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
@@ -1425,3 +1450,35 @@ def test_large_message(acfactory) -> 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()
|
||||
|
||||
132
deltachat-rpc-client/tests/test_sql_schema_docs.py
Normal file
132
deltachat-rpc-client/tests/test_sql_schema_docs.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""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)
|
||||
@@ -1,5 +1,5 @@
|
||||
def test_vcard(acfactory) -> None:
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
def test_vcard(acf) -> None:
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
|
||||
bob.create_chat(alice)
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
def test_webxdc(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_webxdc(acf, rpcdata) -> None:
|
||||
alice, bob = acf.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="../test-data/webxdc/chess.xdc")
|
||||
alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.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(acfactory) -> None:
|
||||
"isAppSender": False,
|
||||
"isBroadcast": False,
|
||||
"sendUpdateInterval": 1000,
|
||||
"sendUpdateMaxSize": 18874368,
|
||||
"sendUpdateMaxSize": 2**20 * (30 - 1) * 3 // 4,
|
||||
}
|
||||
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
@@ -43,12 +43,12 @@ def test_webxdc(acfactory) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_webxdc_insert_lots_of_updates(acf, rpcdata) -> None:
|
||||
alice, bob = acf.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="../test-data/webxdc/chess.xdc")
|
||||
message = alice_chat_bob.send_message(text="Let's play chess!", file=rpcdata.get_path("webxdc/chess.xdc"))
|
||||
|
||||
for i in range(2000):
|
||||
message.send_webxdc_status_update({"payload": str(i)}, "description")
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.57.0-dev"
|
||||
version = "2.61.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
|
||||
@@ -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 --git https://github.com/chatmail/core/ deltachat-rpc-server
|
||||
cargo install --locked --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
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.57.0-dev"
|
||||
"version": "2.61.0-dev"
|
||||
}
|
||||
|
||||
@@ -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/typescript")}`;
|
||||
`file:${join(expected_cwd, "/../../deltachat-jsonrpc-bindings/typescript")}`;
|
||||
} else {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ENV_VAR_NAME } from "./const.js";
|
||||
|
||||
const cargoInstallCommand =
|
||||
"cargo install --git https://github.com/chatmail/core deltachat-rpc-server";
|
||||
"cargo install --locked --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:
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use deltachat::constants::DC_VERSION_STR;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "deltachat-time"
|
||||
version = "1.0.0"
|
||||
description = "Time-related tools"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
edition = "2018"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "2"
|
||||
syn = "3"
|
||||
quote = "1"
|
||||
|
||||
@@ -14,7 +14,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let gen = quote! {
|
||||
let q = quote! {
|
||||
impl rusqlite::types::ToSql for #name {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let num = *self as i64;
|
||||
@@ -24,7 +24,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
q.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(FromSql)]
|
||||
@@ -32,7 +32,7 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let gen = quote! {
|
||||
let q = quote! {
|
||||
impl rusqlite::types::FromSql for #name {
|
||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
||||
@@ -44,5 +44,5 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
q.into()
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ 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" },
|
||||
@@ -80,6 +81,7 @@ 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" },
|
||||
@@ -90,6 +92,7 @@ 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" },
|
||||
|
||||
827
docs/schema.sql
Normal file
827
docs/schema.sql
Normal file
@@ -0,0 +1,827 @@
|
||||
-- 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
445
flake.nix
@@ -29,11 +29,7 @@
|
||||
rustc = fenixToolchain;
|
||||
};
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
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";
|
||||
version = manifest.version;
|
||||
|
||||
rustSrc = nix-filter.lib {
|
||||
root = ./.;
|
||||
@@ -51,6 +47,7 @@
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
./deltachat-jsonrpc
|
||||
./deltachat-jsonrpc-bindings
|
||||
./deltachat-ratelimit
|
||||
./deltachat-repl
|
||||
./deltachat-rpc-client
|
||||
@@ -83,7 +80,7 @@
|
||||
naersk'.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
inherit version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
@@ -91,221 +88,22 @@
|
||||
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.
|
||||
|
||||
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";
|
||||
};
|
||||
mkWin64RustPackage = pkgs.callPackage ./nix/win64-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.
|
||||
mkWin32RustPackage = pkgs.callPackage ./nix/win32-package.nix {
|
||||
inherit naersk system fenixPkgs version;
|
||||
};
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
mkCrossRustPackage = pkgs.callPackage ./nix/cross-rust-package.nix {
|
||||
inherit nixpkgs arch2targets naersk fenixPkgs system rustSrc version;
|
||||
};
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
};
|
||||
mkAndroidRustPackage = pkgs.callPackage ./nix/android-package.nix {
|
||||
inherit naersk fenixPkgs system rustSrc android version;
|
||||
};
|
||||
|
||||
mkAndroidPackages = arch:
|
||||
let
|
||||
@@ -315,32 +113,7 @@
|
||||
"deltachat-rpc-server-${arch}-android" = rpc-server;
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}-android-wheel" =
|
||||
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'';
|
||||
};
|
||||
mkWheel { inherit rpc-server; arch = "${arch}-android"; };
|
||||
};
|
||||
|
||||
mkRustPackages = arch:
|
||||
@@ -350,34 +123,10 @@
|
||||
{
|
||||
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}" = rpc-server;
|
||||
"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'';
|
||||
};
|
||||
"deltachat-rpc-server-${arch}-wheel" = mkWheel { inherit rpc-server; arch = "${arch}"; };
|
||||
};
|
||||
|
||||
mkWheel = pkgs.callPackage ./nix/wheel.nix { inherit nix-filter version; root = ./.; };
|
||||
in
|
||||
{
|
||||
formatter = pkgs.nixpkgs-fmt;
|
||||
@@ -401,116 +150,29 @@
|
||||
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win64-wheel =
|
||||
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'';
|
||||
};
|
||||
mkWheel { rpc-server = deltachat-rpc-server-win64; arch = "win64"; binaryName = "deltachat-rpc-server.exe"; };
|
||||
|
||||
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win32-wheel =
|
||||
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'';
|
||||
};
|
||||
mkWheel
|
||||
{ rpc-server = deltachat-rpc-server-win32; arch = "win32"; binaryName = "deltachat-rpc-server.exe"; };
|
||||
|
||||
# Run `nix build .#docs` to get C docs generated in `./result/`.
|
||||
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'';
|
||||
};
|
||||
docs = pkgs.callPackage ./nix/c-docs.nix { inherit 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;
|
||||
libdeltachat = pkgs.callPackage ./nix/libdeltachat.nix {
|
||||
inherit fenixToolchain rustSrc cargoLock fenixPkgs 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-rpc-client = pkgs.callPackage ./nix/deltachat-rpc-client.nix {
|
||||
inherit version;
|
||||
};
|
||||
|
||||
deltachat-python =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-python";
|
||||
version = manifest.version;
|
||||
inherit version;
|
||||
src = pkgs.lib.cleanSource ./python;
|
||||
format = "pyproject";
|
||||
buildInputs = [
|
||||
@@ -528,51 +190,12 @@
|
||||
pkgs.python3Packages.requests
|
||||
];
|
||||
};
|
||||
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 =
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
python-docs = pkgs.callPackage ./nix/python-docs.nix {
|
||||
inherit deltachat-python deltachat-rpc-client version;
|
||||
};
|
||||
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
|
||||
];
|
||||
};
|
||||
|
||||
devShells.default = import ./nix/shell.nix { inherit nixpkgs fenix system; };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "format-flowed"
|
||||
version = "1.0.0"
|
||||
description = "format=flowed support"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["email"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user