mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 15:02:11 +03:00
Compare commits
79 Commits
v2.45.0
...
hoc/add-ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4fcb872f7 | ||
|
|
626ac8161a | ||
|
|
28cce5e31d | ||
|
|
3b87e27f34 | ||
|
|
24b21c0588 | ||
|
|
eb666d4cc3 | ||
|
|
ef265689dd | ||
|
|
49223792f9 | ||
|
|
920da083d1 | ||
|
|
8f1bf963b4 | ||
|
|
e33d50b4e0 | ||
|
|
f1dc03a4ee | ||
|
|
5d90cc7a2a | ||
|
|
68e630eb82 | ||
|
|
ef718bb869 | ||
|
|
f1860f90d4 | ||
|
|
a947f4296f | ||
|
|
8c3139f7a2 | ||
|
|
3dd7defaa1 | ||
|
|
3096dd6027 | ||
|
|
ee62d2d281 | ||
|
|
6095971f67 | ||
|
|
32ff5b7a6b | ||
|
|
b87805ab24 | ||
|
|
c8716ad85a | ||
|
|
4dd0ba2c72 | ||
|
|
a24248a90b | ||
|
|
af16fc9038 | ||
|
|
c99b8a4482 | ||
|
|
76e2c36d85 | ||
|
|
1b8bf4ed23 | ||
|
|
c553357c60 | ||
|
|
ebe8550c52 | ||
|
|
2637c3bea4 | ||
|
|
d1f1633c60 | ||
|
|
98b55ec15f | ||
|
|
6a3ef20a99 | ||
|
|
59be03a7eb | ||
|
|
8528184fa3 | ||
|
|
5ab1fdca2e | ||
|
|
f616d1bd6c | ||
|
|
e885e052c3 | ||
|
|
6b1e62faba | ||
|
|
7b9e7ae611 | ||
|
|
aedc60f1cc | ||
|
|
017099215c | ||
|
|
e86b170969 | ||
|
|
452ac8a1bc | ||
|
|
5d06ca3c8e | ||
|
|
bdc9e7ce56 | ||
|
|
e30d833c94 | ||
|
|
16668b45e9 | ||
|
|
b148be2618 | ||
|
|
191e6c2821 | ||
|
|
7b700591f4 | ||
|
|
98f03743c6 | ||
|
|
bcaf1284e2 | ||
|
|
fba4e63961 | ||
|
|
810dab12dc | ||
|
|
c0cc2ae816 | ||
|
|
528305e12b | ||
|
|
6e0586058d | ||
|
|
296ed6d74a | ||
|
|
8116460f14 | ||
|
|
52f4293bc5 | ||
|
|
cff0192e38 | ||
|
|
6f17a86903 | ||
|
|
4eb77d5a83 | ||
|
|
e06372c954 | ||
|
|
50cd2514cd | ||
|
|
ba00251572 | ||
|
|
e690186236 | ||
|
|
e14151d6cc | ||
|
|
c6cdccdb97 | ||
|
|
822a99ea9c | ||
|
|
bf02785a36 | ||
|
|
01b2aa0f66 | ||
|
|
fb46c34b55 | ||
|
|
9393753190 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -174,7 +174,7 @@ jobs:
|
||||
run: cargo build -p deltachat_ffi
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||
|
||||
16
.github/workflows/deltachat-rpc-server.yml
vendored
16
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
path: result/bin/deltachat-rpc-server
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
path: result/*.whl
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||
path: result/bin/deltachat-rpc-server.exe
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
path: result/*.whl
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
path: result/bin/deltachat-rpc-server
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
path: result/*.whl
|
||||
@@ -496,7 +496,7 @@ jobs:
|
||||
ls -lah
|
||||
|
||||
- name: Upload to artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-npm-package
|
||||
path: deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -10,7 +10,7 @@ permissions:
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
working-directory: deltachat-rpc-client
|
||||
run: python3 -m build
|
||||
- name: Store the distribution packages
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: deltachat-rpc-client/dist/
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: repl.exe
|
||||
path: "result/bin/deltachat-repl.exe"
|
||||
|
||||
53
.github/workflows/upload-docs.yml
vendored
53
.github/workflows/upload-docs.yml
vendored
@@ -1,16 +1,18 @@
|
||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, py.delta.chat and cffi.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- build_jsonrpc_docs_ci
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build-rs:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: rs.delta.chat
|
||||
url: https://rs.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -23,12 +25,15 @@ jobs:
|
||||
- name: Upload to rs.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:/var/www/html/rs.delta.chat/"
|
||||
|
||||
build-python:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: py.delta.chat
|
||||
url: https://py.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -42,12 +47,15 @@ jobs:
|
||||
- name: Upload to py.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:/var/www/html/py.delta.chat"
|
||||
|
||||
build-c:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: c.delta.chat
|
||||
url: https://c.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -61,12 +69,16 @@ jobs:
|
||||
- name: Upload to c.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:/var/www/html/c.delta.chat"
|
||||
|
||||
build-ts:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: js.jsonrpc.delta.chat
|
||||
url: https://js.jsonrpc.delta.chat/
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
@@ -90,6 +102,27 @@ jobs:
|
||||
- name: Upload to js.jsonrpc.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"
|
||||
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:/var/www/html/js.jsonrpc.delta.chat/"
|
||||
|
||||
build-cffi:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: cffi.delta.chat
|
||||
url: https://cffi.delta.chat/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
- name: Upload to cffi.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CFFI_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/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:/var/www/html/cffi.delta.chat/"
|
||||
|
||||
31
.github/workflows/upload-ffi-docs.yml
vendored
31
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,31 +0,0 @@
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat_ffi` crate documentation
|
||||
# and upload it to <https://cffi.delta.chat/>
|
||||
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
- name: Upload to cffi.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
|
||||
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@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
|
||||
134
CHANGELOG.md
134
CHANGELOG.md
@@ -1,5 +1,136 @@
|
||||
# Changelog
|
||||
|
||||
## [2.48.0] - 2026-03-30
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix reordering problems in multi-relay setups by not sorting received messages below the last seen one.
|
||||
- Always sort "Messages are end-to-end encrypted" notice to the beginning.
|
||||
- Make Message-ID of pre-messages stable across resends ([#8007](https://github.com/chatmail/core/pull/8007)).
|
||||
- Delete `imap_markseen` entries not corresponding to any `imap` rows.
|
||||
- Cleanup `imap` and `imap_sync` records without transport in housekeeping.
|
||||
- When receiving MDN, mark all preceding messages as noticed, even having same timestamp ([#7928](https://github.com/chatmail/core/pull/7928)).
|
||||
- Remove migration 108 preventing upgrades from core 1.86.0 to the latest version.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Improve IMAP loop logs.
|
||||
- Add decryption error to the device message about outgoing message decryption failure.
|
||||
- Log received message sort timestamp.
|
||||
|
||||
### Performance
|
||||
|
||||
- Move sorting outside of SQL query in `store_seen_flags_on_imap`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC API `markfresh_chat()`.
|
||||
- ffi: Correctly declare `dc_event_channel_new()` as having no params ([#7831](https://github.com/chatmail/core/pull/7831)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `wal_checkpoint_mutex`, lock `write_mutex` before getting sql connection instead.
|
||||
- Replace async `RwLock` with sync `RwLock` for stock strings.
|
||||
- Cleanup remaining Autocrypt Setup Message processing in `mimeparser`.
|
||||
- SecureJoin: do not check for self address in forwarding protection.
|
||||
- Fix clippy warnings.
|
||||
|
||||
### CI
|
||||
|
||||
- Update {c,py}.delta.chat website deployments.
|
||||
- Use environments for {rs,cffi,js.jsonrpc}.delta.chat deployments.
|
||||
- Fix https://docs.zizmor.sh/audits/#bot-conditions.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add SQL performance tips to STYLE.md.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove `test_old_message_5`.
|
||||
- Do not rely on loading newest chat in `load_imf_email()`.
|
||||
- Use `load_imf_email()` more.
|
||||
- The message is sorted correctly in the chat even if it arrives late.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: update rustls-webpki to 0.103.10.
|
||||
|
||||
## [2.47.0] - 2026-03-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't fall into infinite loop if the folder is missing ([#8021](https://github.com/chatmail/core/pull/8021)).
|
||||
- Delete `available_post_msgs` row if the message is already downloaded.
|
||||
- Delete `available_post_msgs` row if there is no corresponding IMAP entry.
|
||||
- Make newlines work in chat descriptions ([#8012](https://github.com/chatmail/core/pull/8012)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- use SEIPDv2 if all recipients support it.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add shadowsocks spec to standards.md.
|
||||
- Document Header Confidentiality Policy.
|
||||
- `deltachat_rpc_client`: make sphinx documentation display method parameters.
|
||||
- Remove `draft/aeap-mvp.md` which is superseded by key-contacts and multi-relay.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove code to send messages without intended recipient fingerprint.
|
||||
|
||||
### Tests
|
||||
|
||||
- Make `add_or_lookup_contact_id_no_key` public.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: bump sdp from 0.10.0 to 0.17.1.
|
||||
- Add RUSTSEC-2026-0049 exception to deny.toml.
|
||||
|
||||
## [2.46.0] - 2026-03-19
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove functions for sending and receiving Autocrypt Setup Message.
|
||||
- Add `list_transports_ex()` and `set_transport_unpublished()` functions.
|
||||
- Add API `dc_markfresh_chat` to mark messages as "fresh".
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- add `IncomingCallAccepted.from_this_device`.
|
||||
- decode `dcaccount://` URLs and error out on empty URLs early.
|
||||
- enable anonymous OpenPGP key IDs.
|
||||
- tls: do not verify TLS certificates for hostnames starting with `_`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Mark call message as seen when accepting/declining a call ([#7842](https://github.com/chatmail/core/pull/7842)).
|
||||
- do not send MDNs for hidden messages.
|
||||
- call sync_all() instead of sync_data() when writing accounts.toml.
|
||||
- fsync() the rename() of accounts.toml.
|
||||
- count recipients by Intended Recipient Fingerprints.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: bump zizmorcore/zizmor-action from 0.5.0 to 0.5.2.
|
||||
- cargo: bump astral-tokio-tar from 0.5.6 to 0.6.0.
|
||||
- deps: bump actions/upload-artifact from 6 to 7.
|
||||
- cargo: bump blake3 from 1.8.2 to 1.8.3.
|
||||
- add constant_time_eq 0.3.1 to deny.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- use re-exported rustls::pki_types.
|
||||
- import tokio_rustls::rustls.
|
||||
- Move transport_tests to their own file.
|
||||
|
||||
### Tests
|
||||
|
||||
- Shift time even more in flaky test_sync_broadcast_and_send_message.
|
||||
- test markfresh_chat()
|
||||
|
||||
## [2.45.0] - 2026-03-14
|
||||
|
||||
### API-Changes
|
||||
@@ -7907,3 +8038,6 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.43.0]: https://github.com/chatmail/core/compare/v2.42.0..v2.43.0
|
||||
[2.44.0]: https://github.com/chatmail/core/compare/v2.43.0..v2.44.0
|
||||
[2.45.0]: https://github.com/chatmail/core/compare/v2.44.0..v2.45.0
|
||||
[2.46.0]: https://github.com/chatmail/core/compare/v2.45.0..v2.46.0
|
||||
[2.47.0]: https://github.com/chatmail/core/compare/v2.46.0..v2.47.0
|
||||
[2.48.0]: https://github.com/chatmail/core/compare/v2.47.0..v2.48.0
|
||||
|
||||
59
Cargo.lock
generated
59
Cargo.lock
generated
@@ -194,9 +194,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.5.6"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5"
|
||||
checksum = "3c23f3af104b40a3430ccb90ed5f7bd877a8dc5c26fc92fde51a22b40890dcf9"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"futures-core",
|
||||
@@ -497,15 +497,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.2"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
|
||||
checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"constant_time_eq 0.4.2",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -963,6 +964,12 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.5.0"
|
||||
@@ -1300,7 +1307,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1358,7 +1365,6 @@ dependencies = [
|
||||
"ratelimit",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"rustls-pki-types",
|
||||
"sanitize-filename",
|
||||
"sdp",
|
||||
"serde",
|
||||
@@ -1410,7 +1416,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1431,7 +1437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1447,7 +1453,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1476,7 +1482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -2980,7 +2986,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"ring",
|
||||
"rustls",
|
||||
"rustls-webpki",
|
||||
"rustls-webpki 0.102.8",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"spki",
|
||||
@@ -3026,7 +3032,7 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"constant_time_eq 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3169,7 +3175,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"rustls-webpki",
|
||||
"rustls-webpki 0.102.8",
|
||||
"serde",
|
||||
"sha1",
|
||||
"strum 0.26.2",
|
||||
@@ -5150,15 +5156,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.23"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"rustls-webpki 0.103.10",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5193,6 +5199,17 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
@@ -5301,9 +5318,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sdp"
|
||||
version = "0.10.0"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32c374dceda16965d541c8800ce9cc4e1c14acfd661ddf7952feeedc3411e5c6"
|
||||
checksum = "22c3b0257608d7de4de4c4ea650ccc2e6e3e45e3cd80039fcdee768bcb449253"
|
||||
dependencies = [
|
||||
"rand 0.9.2",
|
||||
"substring",
|
||||
@@ -6165,9 +6182,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.2"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
@@ -86,9 +86,8 @@ rand-old = { package = "rand", version = "0.8" }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rustls-pki-types = "1.12.0"
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.10.0"
|
||||
sdp = "0.17.1"
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -104,7 +103,7 @@ thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.5.6", default-features = false }
|
||||
astral-tokio-tar = { version = "0.6", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
|
||||
6
STYLE.md
6
STYLE.md
@@ -68,6 +68,12 @@ keyword doesn't help here.
|
||||
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
|
||||
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
|
||||
|
||||
When changing complex SQL queries, test them on a new database with `EXPLAIN QUERY PLAN`
|
||||
to make sure that indexes are used and large tables are not going to be scanned.
|
||||
Never run `ANALYZE` on the databases,
|
||||
this makes query planner unpredictable
|
||||
and may make performance significantly worse: <https://github.com/chatmail/core/issues/6585>
|
||||
|
||||
## Errors
|
||||
|
||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -364,18 +364,14 @@ uint32_t dc_get_id (dc_context_t* context);
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_get_next_event() on the emitter.
|
||||
*
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per context.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* may or may not be available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
|
||||
|
||||
@@ -1569,7 +1565,7 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
* (read receipts aren't sent for noticed messages).
|
||||
*
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
* See also dc_markseen_msgs().
|
||||
* See also dc_markseen_msgs() and dc_markfresh_chat().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
@@ -1578,6 +1574,29 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
void dc_marknoticed_chat (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Mark the last incoming message in chat as _fresh_.
|
||||
*
|
||||
* UI can use this to offer a "mark unread" option,
|
||||
* so that already noticed chats (see dc_marknoticed_chat()) get a badge counter again.
|
||||
*
|
||||
* dc_get_fresh_msg_cnt() and dc_get_fresh_msgs() usually is increased by one afterwards.
|
||||
*
|
||||
* #DC_EVENT_MSGS_CHANGED is fired as usual,
|
||||
* however, #DC_EVENT_INCOMING_MSG is _not_ fired again.
|
||||
* This is to not add complexity to incoming messages code,
|
||||
* e.g. UI usually does not add notifications for manually unread chats.
|
||||
* If the UI wants to update system badge counters,
|
||||
* they should do so directly after calling dc_markfresh_chat().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The chat ID of which the last incoming message should be marked as fresh.
|
||||
* If the chat does not have incoming messages, nothing happens.
|
||||
*/
|
||||
void dc_markfresh_chat (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Returns all message IDs of the given types in a given chat or any chat.
|
||||
* Typically used to show a gallery.
|
||||
@@ -2471,76 +2490,6 @@ void dc_imex (dc_context_t* context, int what, c
|
||||
char* dc_imex_has_backup (dc_context_t* context, const char* dir);
|
||||
|
||||
|
||||
/**
|
||||
* Initiate Autocrypt Setup Transfer.
|
||||
* Before starting the setup transfer with this function, the user should be asked:
|
||||
*
|
||||
* ~~~
|
||||
* "An 'Autocrypt Setup Message' securely shares your end-to-end setup with other Autocrypt-compliant apps.
|
||||
* The setup will be encrypted by a setup code which is displayed here and must be typed on the other device.
|
||||
* ~~~
|
||||
*
|
||||
* After that, this function should be called to send the Autocrypt Setup Message.
|
||||
* The function creates the setup message and adds it to outgoing message queue.
|
||||
* The message is sent asynchronously.
|
||||
*
|
||||
* The required setup code is returned in the following format:
|
||||
*
|
||||
* ~~~
|
||||
* 1234-1234-1234-1234-1234-1234-1234-1234-1234
|
||||
* ~~~
|
||||
*
|
||||
* The setup code should be shown to the user then:
|
||||
*
|
||||
* ~~~
|
||||
* "Your key has been sent to yourself. Switch to the other device and
|
||||
* open the setup message. You should be prompted for a setup code. Type
|
||||
* the following digits into the prompt:
|
||||
*
|
||||
* 1234 - 1234 - 1234 -
|
||||
* 1234 - 1234 - 1234 -
|
||||
* 1234 - 1234 - 1234
|
||||
*
|
||||
* Once you're done, your other device will be ready to use Autocrypt."
|
||||
* ~~~
|
||||
*
|
||||
* On the _other device_ you will call dc_continue_key_transfer() then
|
||||
* for setup messages identified by dc_msg_is_setupmessage().
|
||||
*
|
||||
* For more details about the Autocrypt setup process, please refer to
|
||||
* https://autocrypt.org/en/latest/level1.html#autocrypt-setup-message
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return The setup code. Must be released using dc_str_unref() after usage.
|
||||
* On errors, e.g. if the message could not be sent, NULL is returned.
|
||||
*/
|
||||
char* dc_initiate_key_transfer (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Continue the Autocrypt Key Transfer on another device.
|
||||
*
|
||||
* If you have started the key transfer on another device using dc_initiate_key_transfer()
|
||||
* and you've detected a setup message with dc_msg_is_setupmessage(), you should prompt the
|
||||
* user for the setup code and call this function then.
|
||||
*
|
||||
* You can use dc_msg_get_setupcodebegin() to give the user a hint about the code (useful if the user
|
||||
* has created several messages and should not enter the wrong code).
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The ID of the setup message to decrypt.
|
||||
* @param setup_code The setup code entered by the user. This is the same setup code as returned from
|
||||
* dc_initiate_key_transfer() on the other device.
|
||||
* There is no need to format the string correctly, the function will remove all spaces and other characters and
|
||||
* insert the `-` characters at the correct places.
|
||||
* @return 1=key successfully decrypted and imported; both devices will use the same key now;
|
||||
* 0=key transfer failed e.g. due to a bad setup code.
|
||||
*/
|
||||
int dc_continue_key_transfer (dc_context_t* context, uint32_t msg_id, const char* setup_code);
|
||||
|
||||
|
||||
/**
|
||||
* Signal an ongoing process to stop.
|
||||
*
|
||||
@@ -3370,18 +3319,14 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
|
||||
@@ -4659,7 +4604,10 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_GROUP_IMAGE_CHANGED 3
|
||||
#define DC_INFO_MEMBER_ADDED_TO_GROUP 4
|
||||
#define DC_INFO_MEMBER_REMOVED_FROM_GROUP 5
|
||||
|
||||
// Deprecated as of 2026-03-16, not used for new messages.
|
||||
#define DC_INFO_AUTOCRYPT_SETUP_MESSAGE 6
|
||||
|
||||
#define DC_INFO_SECURE_JOIN_MESSAGE 7
|
||||
#define DC_INFO_LOCATIONSTREAMING_ENABLED 8
|
||||
#define DC_INFO_LOCATION_ONLY 9
|
||||
@@ -4689,40 +4637,6 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
char* dc_msg_get_webxdc_href (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the message is an Autocrypt Setup Message.
|
||||
*
|
||||
* Setup messages should be shown in an unique way e.g. using a different text color.
|
||||
* On a click or another action, the user should be prompted for the setup code
|
||||
* which is forwarded to dc_continue_key_transfer() then.
|
||||
*
|
||||
* Setup message are typically generated by dc_initiate_key_transfer() on another device.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return 1=message is a setup message, 0=no setup message.
|
||||
* For setup messages, dc_msg_get_viewtype() returns #DC_MSG_FILE.
|
||||
*/
|
||||
int dc_msg_is_setupmessage (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get the first characters of the setup code.
|
||||
*
|
||||
* Typically, this is used to pre-fill the first entry field of the setup code.
|
||||
* If the user has several setup messages, he can be sure typing in the correct digits.
|
||||
*
|
||||
* To check, if a message is a setup message, use dc_msg_is_setupmessage().
|
||||
* To decrypt a secret key from a setup message, use dc_continue_key_transfer().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Typically the first two digits of the setup code or an empty string if unknown.
|
||||
* NULL is never returned. Must be released using dc_str_unref() when done.
|
||||
*/
|
||||
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Gets the error status of the message.
|
||||
* If there is no error associated with the message, NULL is returned.
|
||||
@@ -6037,7 +5951,7 @@ char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const ch
|
||||
* @memberof dc_event_channel_t
|
||||
* @return An event channel wrapper object (dc_event_channel_t).
|
||||
*/
|
||||
dc_event_channel_t* dc_event_channel_new();
|
||||
dc_event_channel_t* dc_event_channel_new(void);
|
||||
|
||||
/**
|
||||
* Release/free the events channel structure.
|
||||
@@ -6057,21 +5971,14 @@ void dc_event_channel_unref(dc_event_channel_t* event_channel);
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_get_next_event() on the emitter.
|
||||
*
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
* Events are broadcasted to all existing event emitters.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*
|
||||
* @memberof dc_event_channel_t
|
||||
* @param The event channel.
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager / event channel.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);
|
||||
|
||||
@@ -6316,7 +6223,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
*
|
||||
* However, for ongoing processes (e.g. dc_configure())
|
||||
* or for functions that are expected to fail (e.g. dc_continue_key_transfer())
|
||||
* or for functions that are expected to fail
|
||||
* it might be better to delay showing these events until the function has really
|
||||
* failed (returned false). It should be sufficient to report only the _last_ error
|
||||
* in a message box then.
|
||||
@@ -6755,6 +6662,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
* @param data2 (int) 1 if the call was accepted from this device (process).
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_CALL_ACCEPTED 2560
|
||||
|
||||
@@ -6783,8 +6691,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
* 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`
|
||||
* or `set_config(configured_addr)`.
|
||||
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`,
|
||||
* `set_transport_unpublished` or `set_config(configured_addr)`.
|
||||
*/
|
||||
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
||||
|
||||
|
||||
@@ -306,20 +306,17 @@ pub unsafe extern "C" fn dc_set_stock_translation(
|
||||
let msg = to_string_lossy(stock_msg);
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match StockMessage::from_u32(stock_id)
|
||||
.with_context(|| format!("Invalid stock message ID {stock_id}"))
|
||||
match StockMessage::from_u32(stock_id)
|
||||
.with_context(|| format!("Invalid stock message ID {stock_id}"))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(id) => ctx
|
||||
.set_stock_translation(id, msg)
|
||||
.context("set_stock_translation failed")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(id) => ctx
|
||||
.set_stock_translation(id, msg)
|
||||
.await
|
||||
.context("set_stock_translation failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => 0,
|
||||
}
|
||||
})
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -680,7 +677,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. }
|
||||
@@ -703,6 +699,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device, ..
|
||||
} => *from_this_device as libc::c_int,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
@@ -1521,6 +1520,23 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_markfresh_chat(context: *mut dc_context_t, chat_id: u32) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_markfresh_chat()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::markfresh_chat(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.context("Failed markfresh chat")
|
||||
.log_err(ctx)
|
||||
.unwrap_or(())
|
||||
})
|
||||
}
|
||||
|
||||
fn from_prim<S, T>(s: S) -> Option<T>
|
||||
where
|
||||
T: FromPrimitive,
|
||||
@@ -2427,45 +2443,6 @@ pub unsafe extern "C" fn dc_imex_has_backup(
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_initiate_key_transfer()");
|
||||
return ptr::null_mut(); // NULL explicitly defined as "error"
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(imex::initiate_key_transfer(ctx))
|
||||
.context("dc_initiate_key_transfer()")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(res) => res.strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_continue_key_transfer(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
setup_code: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || msg_id <= constants::DC_MSG_ID_LAST_SPECIAL || setup_code.is_null() {
|
||||
eprintln!("ignoring careless call to dc_continue_key_transfer()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(imex::continue_key_transfer(
|
||||
ctx,
|
||||
MsgId::new(msg_id),
|
||||
&to_string_lossy(setup_code),
|
||||
))
|
||||
.context("dc_continue_key_transfer")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_stop_ongoing_process(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
@@ -3788,16 +3765,6 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc
|
||||
ffi_msg.message.get_webxdc_href().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_is_setupmessage()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.is_setupmessage().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
@@ -3808,20 +3775,6 @@ pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
ffi_msg.message.has_html().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_setupcodebegin()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on(ffi_msg.message.get_setupcodebegin(ctx))
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::c_char) {
|
||||
if msg.is_null() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -11,8 +11,8 @@ 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, marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat, Chat,
|
||||
ChatId, ChatItem, MessageListOptions,
|
||||
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
|
||||
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||
@@ -68,6 +68,7 @@ use self::types::{
|
||||
},
|
||||
};
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::login_param::TransportListEntry;
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -472,9 +473,7 @@ impl CommandApi {
|
||||
let accounts = self.accounts.read().await;
|
||||
for (stock_id, stock_message) in strings {
|
||||
if let Some(stock_id) = StockMessage::from_u32(stock_id) {
|
||||
accounts
|
||||
.set_stock_translation(stock_id, stock_message)
|
||||
.await?;
|
||||
accounts.set_stock_translation(stock_id, stock_message)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -528,6 +527,7 @@ impl CommandApi {
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||
async fn add_or_update_transport(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -553,7 +553,23 @@ impl CommandApi {
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
/// Use [Self::list_transports_ex()] to additionally query
|
||||
/// whether the transports are marked as 'unpublished'.
|
||||
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()
|
||||
.map(|t| t.param.into())
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
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,6 +587,26 @@ impl CommandApi {
|
||||
ctx.delete_transport(&addr).await
|
||||
}
|
||||
|
||||
/// Change whether the transport is unpublished.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// The default is false, but when the user updates from a version that didn't have this flag,
|
||||
/// existing secondary transports are set to unpublished,
|
||||
/// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
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?;
|
||||
@@ -699,25 +735,6 @@ impl CommandApi {
|
||||
message::estimate_deletion_cnt(&ctx, from_server, seconds).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// autocrypt
|
||||
// ---------------------------------------------
|
||||
|
||||
async fn initiate_autocrypt_key_transfer(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
deltachat::imex::initiate_key_transfer(&ctx).await
|
||||
}
|
||||
|
||||
async fn continue_autocrypt_key_transfer(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
setup_code: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
deltachat::imex::continue_key_transfer(&ctx, MsgId::new(message_id), &setup_code).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// chat list
|
||||
// ---------------------------------------------
|
||||
@@ -1232,6 +1249,15 @@ impl CommandApi {
|
||||
marknoticed_chat(&ctx, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Marks the last incoming message in the chat as _fresh_.
|
||||
///
|
||||
/// UI can use this to offer a "mark unread" option,
|
||||
/// so that already noticed chats get a badge counter again.
|
||||
async fn markfresh_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
markfresh_chat(&ctx, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Returns the message that is immediately followed by the last seen
|
||||
/// message.
|
||||
/// From the point of view of the user this is effectively
|
||||
|
||||
@@ -441,6 +441,8 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
/// The call was accepted from this device (process).
|
||||
from_this_device: bool,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
@@ -634,9 +636,14 @@ impl From<CoreEventType> for EventType {
|
||||
place_call_info,
|
||||
has_video,
|
||||
},
|
||||
CoreEventType::IncomingCallAccepted { msg_id, chat_id } => IncomingCallAccepted {
|
||||
CoreEventType::IncomingCallAccepted {
|
||||
msg_id,
|
||||
chat_id,
|
||||
from_this_device,
|
||||
} => IncomingCallAccepted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
from_this_device,
|
||||
},
|
||||
CoreEventType::OutgoingCallAccepted {
|
||||
msg_id,
|
||||
|
||||
@@ -4,6 +4,16 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use yerpc::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransportListEntry {
|
||||
/// The login data entered by the user.
|
||||
pub param: EnteredLoginParam,
|
||||
/// Whether this transport is set to 'unpublished'.
|
||||
/// See `set_transport_unpublished` / `setTransportUnpublished` for details.
|
||||
pub is_unpublished: bool,
|
||||
}
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
///
|
||||
/// Usually it will be enough to only set `addr` and `password`,
|
||||
@@ -56,6 +66,15 @@ pub struct EnteredLoginParam {
|
||||
pub oauth2: Option<bool>,
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -68,7 +68,6 @@ pub struct MessageObject {
|
||||
/// if `show_padlock` is `false`,
|
||||
/// and nothing if it is `true`.
|
||||
show_padlock: bool,
|
||||
is_setupmessage: bool,
|
||||
is_info: bool,
|
||||
is_forwarded: bool,
|
||||
|
||||
@@ -88,8 +87,6 @@ pub struct MessageObject {
|
||||
override_sender_name: Option<String>,
|
||||
sender: ContactObject,
|
||||
|
||||
setup_code_begin: Option<String>,
|
||||
|
||||
file: Option<String>,
|
||||
file_mime: Option<String>,
|
||||
|
||||
@@ -226,7 +223,6 @@ impl MessageObject {
|
||||
|
||||
subject: message.get_subject().to_owned(),
|
||||
show_padlock: message.get_showpadlock(),
|
||||
is_setupmessage: message.is_setupmessage(),
|
||||
is_info: message.is_info(),
|
||||
is_forwarded: message.is_forwarded(),
|
||||
is_bot: message.is_bot(),
|
||||
@@ -243,8 +239,6 @@ impl MessageObject {
|
||||
override_sender_name,
|
||||
sender,
|
||||
|
||||
setup_code_begin: message.get_setupcodebegin(context).await,
|
||||
|
||||
file: match message.get_file(context) {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.45.0"
|
||||
"version": "2.48.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -302,9 +302,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
// TODO: reuse commands definition in main.rs.
|
||||
"imex" => println!(
|
||||
"====================Import/Export commands==\n\
|
||||
initiate-key-transfer\n\
|
||||
get-setupcodebegin <msg-id>\n\
|
||||
continue-key-transfer <msg-id> <setup-code>\n\
|
||||
has-backup\n\
|
||||
export-backup\n\
|
||||
import-backup <backup-file>\n\
|
||||
@@ -408,34 +405,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
============================================="
|
||||
),
|
||||
},
|
||||
"initiate-key-transfer" => match initiate_key_transfer(&context).await {
|
||||
Ok(setup_code) => {
|
||||
println!("Setup code for the transferred setup message: {setup_code}",)
|
||||
}
|
||||
Err(err) => bail!("Failed to generate setup code: {err}"),
|
||||
},
|
||||
"get-setupcodebegin" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let msg_id: MsgId = MsgId::new(arg1.parse()?);
|
||||
let msg = Message::load_from_db(&context, msg_id).await?;
|
||||
if msg.is_setupmessage() {
|
||||
let setupcodebegin = msg.get_setupcodebegin(&context).await;
|
||||
println!(
|
||||
"The setup code for setup message {} starts with: {}",
|
||||
msg_id,
|
||||
setupcodebegin.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
bail!("{msg_id} is no setup message.",);
|
||||
}
|
||||
}
|
||||
"continue-key-transfer" => {
|
||||
ensure!(
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <setup-code> expected"
|
||||
);
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
|
||||
}
|
||||
"has-backup" => {
|
||||
has_backup(&context, blobdir).await?;
|
||||
}
|
||||
|
||||
@@ -149,10 +149,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 13] = [
|
||||
"initiate-key-transfer",
|
||||
"get-setupcodebegin",
|
||||
"continue-key-transfer",
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -187,13 +187,9 @@ class futuremethod: # noqa: N801
|
||||
"""Decorator for async methods."""
|
||||
|
||||
def __init__(self, func):
|
||||
functools.update_wrapper(self, func)
|
||||
self._func = func
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
def future(*args):
|
||||
generator = self._func(instance, *args)
|
||||
res = next(generator)
|
||||
@@ -206,6 +202,7 @@ class futuremethod: # noqa: N801
|
||||
|
||||
return f
|
||||
|
||||
@functools.wraps(self._func)
|
||||
def wrapper(*args):
|
||||
f = future(*args)
|
||||
return f()
|
||||
|
||||
@@ -483,10 +483,6 @@ class Account:
|
||||
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||
|
||||
def initiate_autocrypt_key_transfer(self) -> None:
|
||||
"""Send Autocrypt Setup Message."""
|
||||
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
||||
|
||||
def ice_servers(self) -> list:
|
||||
"""Return ICE servers for WebRTC configuration."""
|
||||
ice_servers_json = self._rpc.ice_servers(self.id)
|
||||
|
||||
@@ -219,6 +219,10 @@ class Chat:
|
||||
"""Mark all messages in this chat as noticed."""
|
||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
|
||||
def mark_fresh(self) -> None:
|
||||
"""Mark the last incoming message in the chat as fresh."""
|
||||
self._rpc.markfresh_chat(self.account.id, self.id)
|
||||
|
||||
def add_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
||||
"""Add contacts to this group."""
|
||||
from .account import Account
|
||||
|
||||
@@ -72,14 +72,6 @@ class Message:
|
||||
"""Return True if the message exists."""
|
||||
return bool(self._rpc.get_existing_msg_ids(self.account.id, [self.id]))
|
||||
|
||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||
"""Continue the Autocrypt Setup Message key transfer.
|
||||
|
||||
This function can be called on received Autocrypt Setup Message
|
||||
to import the key encrypted with the provided setup code.
|
||||
"""
|
||||
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
|
||||
|
||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def wait_for_autocrypt_setup_message(account):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0:
|
||||
msg_id = event.msg_id
|
||||
msg = account.get_message_by_id(msg_id)
|
||||
if msg.get_snapshot().is_setupmessage:
|
||||
return msg
|
||||
|
||||
|
||||
def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
alice2.bring_online()
|
||||
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
# Test that entering wrong code returns an error.
|
||||
with pytest.raises(JsonRpcError):
|
||||
msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756")
|
||||
|
||||
msg.continue_autocrypt_key_transfer(setup_code)
|
||||
|
||||
|
||||
def test_ac_setup_message_twice(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
alice2.bring_online()
|
||||
|
||||
# Send the first Autocrypt Setup Message and ignore it.
|
||||
_setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
# Send the second Autocrypt Setup Message and import it.
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
msg.continue_autocrypt_key_transfer(setup_code)
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import DownloadState
|
||||
from deltachat_rpc_client.const import ChatType, DownloadState
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ def test_message_info_imap_urls(acfactory) -> None:
|
||||
assert f"{new_alice_addr}/INBOX" in msg_info
|
||||
|
||||
|
||||
def test_remove_primary_transport(acfactory) -> None:
|
||||
def test_remove_primary_transport(acfactory, 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()
|
||||
@@ -368,7 +368,7 @@ def test_remove_primary_transport(acfactory) -> None:
|
||||
bob_chat = bob.create_chat(alice)
|
||||
alice.create_chat(bob)
|
||||
|
||||
# Alice changes the transport.
|
||||
log.section("Alice sets up second transport")
|
||||
[transport1, transport2] = alice.list_transports()
|
||||
alice.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
@@ -376,7 +376,7 @@ def test_remove_primary_transport(acfactory) -> None:
|
||||
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg1.text == "Hello!"
|
||||
|
||||
# Alice deletes the first transport.
|
||||
log.section("Alice removes the primary relay")
|
||||
alice.delete_transport(transport1["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
@@ -384,3 +384,5 @@ def test_remove_primary_transport(acfactory) -> None:
|
||||
bob_chat.send_text("Hello again!")
|
||||
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)
|
||||
|
||||
@@ -273,6 +273,9 @@ def test_chat(acfactory) -> None:
|
||||
assert group.get_messages()
|
||||
group.get_fresh_message_count()
|
||||
group.mark_noticed()
|
||||
assert group.get_fresh_message_count() == 0
|
||||
group.mark_fresh()
|
||||
assert group.get_fresh_message_count() > 0
|
||||
assert group.get_contacts()
|
||||
assert group.get_past_contacts() == []
|
||||
group.remove_contact(alice_contact_bob)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.45.0"
|
||||
"version": "2.48.0"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,13 @@ ignore = [
|
||||
# It is a transitive dependency of iroh 0.35.0,
|
||||
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||
"RUSTSEC-2025-0134",
|
||||
|
||||
# rustls-webpki v0.102.8
|
||||
# We cannot upgrade to >=0.103.10 because
|
||||
# it is a transitive dependency of iroh 0.35.0
|
||||
# which depends on ^0.102.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0049>
|
||||
"RUSTSEC-2026-0049",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -27,6 +34,7 @@ ignore = [
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "constant_time_eq", version = "0.3.1" },
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
@@ -41,6 +49,7 @@ skip = [
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "rustls-webpki", version = "0.102.8" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "socket2", version = "0.5.9" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- [x] We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- [x] If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- [x] No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
||||
|
||||
- [ ] When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
||||
|
||||
- [x] ([#3385](https://github.com/deltachat/deltachat-core-rust/pull/3385)) When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
||||
AND there is a `Chat-Version` header\
|
||||
AND the message is signed correctly
|
||||
AND the From address is (also) in the encrypted (and therefore signed) headers <sup>[[1]](#myfootnote1)</sup>\
|
||||
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
|
||||
|
||||
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
|
||||
|
||||
<a name="myfootnote1">[1]</a>: Without this check, an attacker could replay a message from Alice to Bob. Then Bob's device would do an AEAP transition from Alice's to the attacker's address, allowing for easier phishing.
|
||||
|
||||
<details>
|
||||
<summary>More details about this</summary>
|
||||
Suppose Alice sends a message to Evil (or to a group with both Evil and Bob). Evil then forwards the message to Bob, changing the From and To headers (and if necessary Message-Id) and replacing `addr=alice@example.org;` in the autocrypt header with `addr=evil@example.org;`.
|
||||
|
||||
Then Bob's device sees that there is a message which is signed by Alice's key and comes from Evil's address and would do the AEAP transition, i.e. replace Alice with Evil in all groups and show a message "Alice changed their address from alice@example.org to evil@example.org". Disadvantages for Evil are that Bob's message will be shown on Alice's device, possibly creating confusion/suspicion, and that the usual "Setup changed for..." message will be shown the next time Evil sends a message (because Evil doesn't know Alice's private key).
|
||||
|
||||
Possible mitigations:
|
||||
- if we make the AEAP device message sth. like "Automatically removed alice@example.org and added evil@example.org", then this will create more suspicion, making the phishing harder (we didn't talk about what what the wording should be at all yet).
|
||||
- Add something similar to replay protection to our Autocrypt implementation. This could be done e.g. by adding a second `From` header to the protected headers. If it's present, the receiver then requires it to be the same as the outer `From`, and if it's not present, we don't do AEAP --> **That's what we implemented**
|
||||
|
||||
Note that usually a mail is signed by a key that has a UID matching the from address.
|
||||
|
||||
That's not mandatory for Autocrypt (and in fact, we just keep the old UID when changing the self address, so with AEAP the UID will actually be different than the from address sometimes)
|
||||
|
||||
https://autocrypt.org/level1.html#openpgp-based-key-data says:
|
||||
> The content of the user id packet is only decorative
|
||||
|
||||
</details>
|
||||
|
||||
### Notes:
|
||||
|
||||
- We treat protected and non-protected chats the same
|
||||
- We leave the aeap transition statement away since it seems not to be needed, makes things harder on the sending side, wastes some network traffic, and is worse for privacy (since more people know what old addresses you had).
|
||||
- As soon as we encrypt read receipts, sending a read receipt will be enough to tell a lot of people that you transitioned
|
||||
- AEAP will make the problem of inconsistent group state worse, both because it doesn't work if the message is unencrypted (even if the design allowed it, it would be problematic security-wise) and because some chat partners may have gotten the transition and some not. We should do something against this at some point in the future, like asking the user whether they want to add/remove the members to restore consistent group state.
|
||||
|
||||
#### Downsides of this design:
|
||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
||||
|
||||
#### Upsides:
|
||||
- With this approach, it's easy to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- Faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.
|
||||
|
||||
### Alternatives and old discussions/plans:
|
||||
|
||||
- Change the contact instead of rewriting the group member lists. This seems to call for more trouble since we will end up with multiple contacts having the same email address.
|
||||
|
||||
- If needed, we could add a header a) indicating that the sender did an address transition or b) listing all the secondary (old) addresses. For now, there is no big enough benefit to warrant introducing another header and its processing on the receiver side (including all the necessary checks and handling of error cases). Instead, we only check for the `Chat-Version` header to prevent accidental transitions when an MUA user sends a message from another email address with the same key.
|
||||
|
||||
- The condition for a transition temporarily was:
|
||||
|
||||
> When receiving a message: If we are going to assign a message to a chat, but the sender is not a member of this chat\
|
||||
> AND the signing key is the same as the direct (non-gossiped) key of one of the chat members\
|
||||
> AND ...
|
||||
|
||||
However, this would mean that in 1:1 messages can't trigger a transition, since we don't assign private messages to the parent chat, but always to the 1:1 chat with the sender.
|
||||
|
||||
<details>
|
||||
<summary>Some previous state of the discussion, which temporarily lived in an issue description</summary>
|
||||
Summarizing the discussions from https://github.com/deltachat/deltachat-core-rust/pull/2896, mostly quoting @hpk42:
|
||||
|
||||
1. (DONE) At the time of configure we push the current primary to become a secondary.
|
||||
|
||||
2. When a message is sent out to a chat, and the message is encrypted, and we have secondary addresses, then we
|
||||
a) add a protected "AEAP-Replacement" header that contains all secondary addresses
|
||||
b) if any of the secondary addresses is in the chat's member list, we remove it and leave a system message that we did so
|
||||
3. When an encrypted message with a replacement header is received, replace the e-mail address of all secondary contacts (if they exist) with the new primary and drop a sysmessage in all chats the secondary is member off. This might (in edge cases) result in chats that have two or more contacts with the same e-mail address. We might ignore this for a first release and just log a warning. Let's maybe not get hung up on this case before everything else works.
|
||||
|
||||
Notes:
|
||||
- for now we will send out aeap replacement headers forever, there is no termination condition other than lack of secondary addresses. I think that's fine for now. Later on we might introduce options to remove secondary addresses but i wouldn't do this for a first release/PR.
|
||||
- the design is resilient against changing e-mail providers from A to B to C and then back to A, with partially updated chats and diverging views from recipients/contacts on this transition. In the end, you will have a primary and some secondaries, and when you start sending out messages everybody will eventually synchronize when they receive the current state of primaries/secondaries.
|
||||
- of course on incoming message for need to check for each stated secondary address in the replacement header that it uses the same signature as the signature we verified as valid with the incoming message **--> Also we have to somehow make sure that the signing key was not just gossiped from some random other person in some group.**
|
||||
- there are no extra flags/columns in the database needed (i hope)
|
||||
|
||||
#### Downsides of the chosen approach:
|
||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
||||
- There will be multiple contacts with the same address in the database. We will have to do something against this at some point.
|
||||
|
||||
The most obvious alternative would be to create a new contact with the new address and replace the old contact in the groups.
|
||||
|
||||
#### Upsides:
|
||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
|
||||
|
||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
||||
|
||||
_end of the previous state of the discussion_
|
||||
|
||||
</details>
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
|
||||
Notes during implementing
|
||||
========================
|
||||
|
||||
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.45.0"
|
||||
version = "2.48.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -553,17 +553,6 @@ class Account:
|
||||
def imex(self, path: str, imex_cmd: int, passphrase: Optional[str] = None) -> None:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), as_dc_charpointer(passphrase))
|
||||
|
||||
def initiate_key_transfer(self) -> str:
|
||||
"""return setup code after a Autocrypt setup message
|
||||
has been successfully sent to our own e-mail address ("self-sent message").
|
||||
If sending out was unsuccessful, a RuntimeError is raised.
|
||||
"""
|
||||
self.check_is_configured()
|
||||
res = lib.dc_initiate_key_transfer(self._dc_context)
|
||||
if res == ffi.NULL:
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self) -> str:
|
||||
"""get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
|
||||
@@ -167,14 +167,6 @@ class Message:
|
||||
"""return True if this message is a system/info message."""
|
||||
return bool(lib.dc_msg_is_info(self._dc_msg))
|
||||
|
||||
def is_setup_message(self):
|
||||
"""return True if this message is a setup message."""
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
|
||||
def get_setupcodebegin(self) -> str:
|
||||
"""return the first characters of a setup code in a setup message."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
||||
|
||||
def is_encrypted(self):
|
||||
"""return True if this message was encrypted."""
|
||||
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
|
||||
@@ -198,12 +190,6 @@ class Message:
|
||||
"""Get a message summary as a single line of text. Typically used for notifications."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_summarytext(self._dc_msg, width))
|
||||
|
||||
def continue_key_transfer(self, setup_code):
|
||||
"""extract key and use it as primary key for this account."""
|
||||
res = lib.dc_continue_key_transfer(self.account._dc_context, self.id, as_dc_charpointer(setup_code))
|
||||
if res == 0:
|
||||
raise ValueError("Importing the key from Autocrypt Setup Message failed")
|
||||
|
||||
@props.with_doc
|
||||
def time_sent(self):
|
||||
"""UTC time when the message was sent.
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-03-14
|
||||
2026-03-30
|
||||
21
spec.md
21
spec.md
@@ -39,9 +39,24 @@ Messages SHOULD be encrypted by the
|
||||
[Autocrypt](https://autocrypt.org/level1.html) standard;
|
||||
`prefer-encrypt=mutual` MAY be set by default.
|
||||
|
||||
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
|
||||
by the [Protected Headers](https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
|
||||
|
||||
Meta data SHOULD be encrypted
|
||||
by the [Header Protection](https://www.rfc-editor.org/rfc/rfc9788.html) standard
|
||||
with the following [Header Confidentiality Policy](https://www.rfc-editor.org/rfc/rfc9788.html#name-header-confidentiality-poli):
|
||||
```
|
||||
hcp_chat(name, val_in) → val_out:
|
||||
if lower(name) is 'from':
|
||||
assert that val_in is an RFC 5322 mailbox
|
||||
return the RFC 5322 addr-spec part of val_in
|
||||
else if lower(name) is 'to':
|
||||
return '"hidden-recipients": ;'
|
||||
else if lower(name) is 'date':
|
||||
return the UTC form of a random date within the last 7 days
|
||||
else if lower(name) is 'subject':
|
||||
return '[...]'
|
||||
else if lower(name) is in ['message-id', 'chat-is-post-message']:
|
||||
return val_in
|
||||
return null
|
||||
```
|
||||
|
||||
# Outgoing messages
|
||||
|
||||
|
||||
@@ -686,13 +686,27 @@ impl Config {
|
||||
file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
|
||||
.await
|
||||
.context("failed to write a tmp config")?;
|
||||
file.sync_data()
|
||||
|
||||
// We use `sync_all()` and not `sync_data()` here.
|
||||
// This translates to `fsync()` instead of `fdatasync()`.
|
||||
// `fdatasync()` may be insufficient for newely created files
|
||||
// and may not even synchronize the file size on some operating systems,
|
||||
// resulting in a truncated file.
|
||||
file.sync_all()
|
||||
.await
|
||||
.context("failed to sync a tmp config")?;
|
||||
drop(file);
|
||||
fs::rename(&tmp_path, &self.file)
|
||||
.await
|
||||
.context("failed to rename config")?;
|
||||
// Sync the rename().
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let parent = self.file.parent().context("No parent directory")?;
|
||||
let parent_file = fs::File::open(parent).await?;
|
||||
parent_file.sync_all().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1235,13 +1249,11 @@ mod tests {
|
||||
let account1 = accounts.get_account(1).context("failed to get account 1")?;
|
||||
let account2 = accounts.get_account(2).context("failed to get account 2")?;
|
||||
|
||||
assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
|
||||
assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
|
||||
account1
|
||||
.set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
|
||||
.await?;
|
||||
assert_eq!(stock_str::no_messages(&account1).await, "foobar");
|
||||
assert_eq!(stock_str::no_messages(&account2).await, "foobar");
|
||||
assert_eq!(stock_str::no_messages(&account1), "No messages.");
|
||||
assert_eq!(stock_str::no_messages(&account2), "No messages.");
|
||||
account1.set_stock_translation(StockMessage::NoMessages, "foobar".to_string())?;
|
||||
assert_eq!(stock_str::no_messages(&account1), "foobar");
|
||||
assert_eq!(stock_str::no_messages(&account2), "foobar");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
37
src/calls.rs
37
src/calls.rs
@@ -11,7 +11,7 @@ use crate::context::{Context, WeakContext};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::message::{Message, MsgId, Viewtype, markseen_msgs};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
use crate::param::Param;
|
||||
@@ -104,13 +104,11 @@ impl CallInfo {
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
let incoming_call_str =
|
||||
stock_str::incoming_call(context, self.has_video_initially()).await;
|
||||
let incoming_call_str = stock_str::incoming_call(context, self.has_video_initially());
|
||||
self.update_text(context, &format!("{incoming_call_str}\n{duration}"))
|
||||
.await?;
|
||||
} else {
|
||||
let outgoing_call_str =
|
||||
stock_str::outgoing_call(context, self.has_video_initially()).await;
|
||||
let outgoing_call_str = stock_str::outgoing_call(context, self.has_video_initially());
|
||||
self.update_text(context, &format!("{outgoing_call_str}\n{duration}"))
|
||||
.await?;
|
||||
}
|
||||
@@ -207,7 +205,7 @@ impl Context {
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
|
||||
let outgoing_call_str = stock_str::outgoing_call(self, has_video_initially).await;
|
||||
let outgoing_call_str = stock_str::outgoing_call(self, has_video_initially);
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Call,
|
||||
text: outgoing_call_str,
|
||||
@@ -249,6 +247,7 @@ impl Context {
|
||||
if chat.is_contact_request() {
|
||||
chat.id.accept(self).await?;
|
||||
}
|
||||
markseen_msgs(self, vec![call_id]).await?;
|
||||
|
||||
// send an acceptance message around: to the caller as well as to the other devices of the callee
|
||||
let mut msg = Message {
|
||||
@@ -265,6 +264,7 @@ impl Context {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
from_this_device: true,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
@@ -283,11 +283,12 @@ impl Context {
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_ended(self).await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
markseen_msgs(self, vec![call_id]).await?;
|
||||
let declined_call_str = stock_str::declined_call(self);
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
let canceled_call_str = stock_str::canceled_call(self).await;
|
||||
let canceled_call_str = stock_str::canceled_call(self);
|
||||
call.update_text(self, &canceled_call_str).await?;
|
||||
}
|
||||
} else {
|
||||
@@ -330,11 +331,11 @@ impl Context {
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
let missed_call_str = stock_str::missed_call(&context).await;
|
||||
let missed_call_str = stock_str::missed_call(&context);
|
||||
call.update_text(&context, &missed_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_ended(&context).await?;
|
||||
let canceled_call_str = stock_str::canceled_call(&context).await;
|
||||
let canceled_call_str = stock_str::canceled_call(&context);
|
||||
call.update_text(&context, &canceled_call_str).await?;
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
@@ -360,12 +361,12 @@ impl Context {
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
let missed_call_str = stock_str::missed_call(self).await;
|
||||
let missed_call_str = stock_str::missed_call(self);
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
|
||||
} else {
|
||||
let incoming_call_str =
|
||||
stock_str::incoming_call(self, call.has_video_initially()).await;
|
||||
stock_str::incoming_call(self, call.has_video_initially());
|
||||
call.update_text(self, &incoming_call_str).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
|
||||
let can_call_me = match who_can_call_me(self).await? {
|
||||
@@ -406,8 +407,7 @@ impl Context {
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let outgoing_call_str =
|
||||
stock_str::outgoing_call(self, call.has_video_initially()).await;
|
||||
let outgoing_call_str = stock_str::outgoing_call(self, call.has_video_initially());
|
||||
call.update_text(self, &outgoing_call_str).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
}
|
||||
@@ -430,6 +430,7 @@ impl Context {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
from_this_device: false,
|
||||
});
|
||||
} else {
|
||||
let accept_call_info = mime_message
|
||||
@@ -458,22 +459,22 @@ impl Context {
|
||||
if call.is_incoming() {
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_ended(self).await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
let declined_call_str = stock_str::declined_call(self);
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
let missed_call_str = stock_str::missed_call(self).await;
|
||||
let missed_call_str = stock_str::missed_call(self);
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_canceled(self).await?;
|
||||
let canceled_call_str = stock_str::canceled_call(self).await;
|
||||
let canceled_call_str = stock_str::canceled_call(self);
|
||||
call.update_text(self, &canceled_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
let declined_call_str = stock_str::declined_call(self);
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
@@ -115,9 +116,28 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
// Bob accepts the incoming call
|
||||
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
|
||||
.await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device: true,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let sent2 = bob.pop_sent_msg().await;
|
||||
let info = bob
|
||||
@@ -131,7 +151,15 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
bob2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device: false,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let info = bob2
|
||||
.load_call_by_id(bob2_call.id)
|
||||
@@ -200,9 +228,20 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
|
||||
// Bob has accepted the call and also ends it
|
||||
bob.end_call(bob_call.id).await?;
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -328,8 +367,18 @@ async fn test_callee_rejects_call() -> Result<()> {
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob has accepted Alice before, but does not want to talk with Alice
|
||||
bob_call.chat_id.accept(&bob).await?;
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Declined call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -370,6 +419,35 @@ async fn test_callee_rejects_call() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_callee_sees_contact_request_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let bob_call = bob.recv_msg(&sent1).await;
|
||||
// Bob can't end_call() because the contact request isn't accepted, but he can mark the call as
|
||||
// seen.
|
||||
markseen_msgs(bob, vec![bob_call.id]).await?;
|
||||
assert_eq!(bob_call.id.get_state(bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN only to self so that an unaccepted contact can't know anything.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, ContactId::SELF)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
|
||||
164
src/chat.rs
164
src/chat.rs
@@ -476,13 +476,18 @@ impl ChatId {
|
||||
|
||||
/// Adds message "Messages are end-to-end encrypted".
|
||||
pub(crate) async fn add_e2ee_notice(self, context: &Context, timestamp: i64) -> Result<()> {
|
||||
let text = stock_str::messages_e2ee_info_msg(context).await;
|
||||
let text = stock_str::messages_e2ee_info_msg(context);
|
||||
|
||||
// Sort this notice to the very beginning of the chat.
|
||||
// We don't want any message to appear before this notice
|
||||
// which is normally added when encrypted chat is created.
|
||||
let sort_timestamp = 0;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&text,
|
||||
SystemMessage::ChatE2ee,
|
||||
Some(timestamp),
|
||||
Some(sort_timestamp),
|
||||
timestamp,
|
||||
None,
|
||||
None,
|
||||
@@ -669,7 +674,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
}
|
||||
|
||||
if chat.is_self_talk() {
|
||||
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
|
||||
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context));
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
@@ -925,6 +930,17 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Returns timestamp of us joining the chat if we are the member of the chat.
|
||||
pub(crate) async fn join_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT add_timestamp FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
(self, ContactId::SELF),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns timestamp of the latest message in the chat,
|
||||
/// including hidden messages or a draft if there is one.
|
||||
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
@@ -1155,10 +1171,10 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
if !chat.is_encrypted(context).await? {
|
||||
return Ok(stock_str::encr_none(context).await);
|
||||
return Ok(stock_str::encr_none(context));
|
||||
}
|
||||
|
||||
let mut ret = stock_str::messages_are_e2ee(context).await + "\n";
|
||||
let mut ret = stock_str::messages_are_e2ee(context) + "\n";
|
||||
|
||||
for &contact_id in get_chat_contacts(context, self)
|
||||
.await?
|
||||
@@ -1212,15 +1228,11 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
/// corresponding event in case of a system message (usually the current system time).
|
||||
/// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
|
||||
/// to the chat bottom.
|
||||
/// `received` -- whether the message is received. Otherwise being sent.
|
||||
/// `incoming` -- whether the message is incoming.
|
||||
pub(crate) async fn calc_sort_timestamp(
|
||||
self,
|
||||
context: &Context,
|
||||
message_timestamp: i64,
|
||||
always_sort_to_bottom: bool,
|
||||
received: bool,
|
||||
incoming: bool,
|
||||
) -> Result<i64> {
|
||||
let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
|
||||
|
||||
@@ -1240,38 +1252,6 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
(self, MessageState::OutDraft),
|
||||
)
|
||||
.await?
|
||||
} else if received {
|
||||
// Received messages shouldn't mingle with just sent ones and appear somewhere in the
|
||||
// middle of the chat, so we go after the newest non fresh message.
|
||||
//
|
||||
// But if a received outgoing message is older than some seen message, better sort the
|
||||
// received message purely by timestamp. We could place it just before that seen
|
||||
// message, but anyway the user may not notice it.
|
||||
//
|
||||
// NB: Received outgoing messages may break sorting of fresh incoming ones, but this
|
||||
// shouldn't happen frequently. Seen incoming messages don't really break sorting of
|
||||
// fresh ones, they rather mean that older incoming messages are actually seen as well.
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
|
||||
FROM msgs
|
||||
WHERE chat_id=? AND hidden=0 AND state>?
|
||||
HAVING COUNT(*) > 0",
|
||||
(MessageState::InSeen, self, MessageState::InFresh),
|
||||
|row| {
|
||||
let ts: i64 = row.get(0)?;
|
||||
let ts_sent_seen: i64 = row.get(1)?;
|
||||
Ok((ts, ts_sent_seen))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.and_then(|(ts, ts_sent_seen)| {
|
||||
match incoming || ts_sent_seen <= message_timestamp {
|
||||
true => Some(ts),
|
||||
false => None,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1282,7 +1262,16 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
sort_timestamp = last_msg_time;
|
||||
}
|
||||
|
||||
Ok(sort_timestamp)
|
||||
if let Some(join_timestamp) = self.join_timestamp(context).await? {
|
||||
// If we are the member of the chat, don't add messages
|
||||
// before the timestamp of us joining it.
|
||||
// This is needed to avoid sorting "Member added"
|
||||
// or automatically sent bot welcome messages
|
||||
// above SecureJoin system messages.
|
||||
Ok(std::cmp::max(sort_timestamp, join_timestamp))
|
||||
} else {
|
||||
Ok(sort_timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1392,7 +1381,7 @@ impl Chat {
|
||||
.context(format!("Failed loading chat {chat_id} from database"))?;
|
||||
|
||||
if chat.id.is_archived_link() {
|
||||
chat.name = stock_str::archived_chats(context).await;
|
||||
chat.name = stock_str::archived_chats(context);
|
||||
} else {
|
||||
if chat.typ == Chattype::Single && chat.name.is_empty() {
|
||||
// chat.name is set to contact.display_name on changes,
|
||||
@@ -1416,9 +1405,9 @@ impl Chat {
|
||||
chat.name = chat_name;
|
||||
}
|
||||
if chat.param.exists(Param::Selftalk) {
|
||||
chat.name = stock_str::saved_messages(context).await;
|
||||
chat.name = stock_str::saved_messages(context);
|
||||
} else if chat.param.exists(Param::Devicetalk) {
|
||||
chat.name = stock_str::device_messages(context).await;
|
||||
chat.name = stock_str::device_messages(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2306,15 +2295,10 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
|
||||
update_special_chat_name(
|
||||
context,
|
||||
ContactId::DEVICE,
|
||||
stock_str::device_messages(context).await,
|
||||
)
|
||||
.await?;
|
||||
update_special_chat_name(
|
||||
context,
|
||||
ContactId::SELF,
|
||||
stock_str::saved_messages(context).await,
|
||||
stock_str::device_messages(context),
|
||||
)
|
||||
.await?;
|
||||
update_special_chat_name(context, ContactId::SELF, stock_str::saved_messages(context)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2844,19 +2828,12 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out:
|
||||
if (msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden)
|
||||
// This may happen eg. for groups with only SELF and bcc_self disabled:
|
||||
|| (!context.get_config_bool(Config::BccSelf).await? && recipients.is_empty())
|
||||
{
|
||||
smtp::add_self_recipients(context, &mut recipients, needs_encryption).await?;
|
||||
}
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out
|
||||
if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden {
|
||||
recipients.clear();
|
||||
}
|
||||
|
||||
if recipients.is_empty() {
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"Message {} has no recipient, skipping smtp-send.", msg.id
|
||||
@@ -2895,6 +2872,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
);
|
||||
}
|
||||
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
smtp::add_self_recipients(context, &mut recipients, rendered_msg.is_encrypted).await?;
|
||||
}
|
||||
|
||||
if needs_encryption && !rendered_msg.is_encrypted {
|
||||
/* unrecoverable */
|
||||
message::set_msg_failed(
|
||||
@@ -3071,7 +3052,7 @@ async fn donation_request_maybe(context: &Context) -> Result<()> {
|
||||
let ts = if ts == 0 || msg_cnt.await? < 100 {
|
||||
now.saturating_add(secs_between_checks)
|
||||
} else {
|
||||
let mut msg = Message::new_text(stock_str::donation_request(context).await);
|
||||
let mut msg = Message::new_text(stock_str::donation_request(context));
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
i64::MAX
|
||||
};
|
||||
@@ -3406,6 +3387,38 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks last incoming message in a chat as fresh.
|
||||
pub async fn markfresh_chat(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
let affected_rows = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET state=?1
|
||||
WHERE id=(SELECT id
|
||||
FROM msgs
|
||||
WHERE state IN (?1, ?2, ?3) AND hidden=0 AND chat_id=?4
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT 1)
|
||||
AND state!=?1",
|
||||
(
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
MessageState::InSeen,
|
||||
chat_id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if affected_rows == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_msg_id(chat_id);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all database message IDs of the given types.
|
||||
///
|
||||
/// If `chat_id` is None, return messages from any chat.
|
||||
@@ -3593,10 +3606,10 @@ pub(crate) async fn create_group_ex(
|
||||
{
|
||||
let text = if !grpid.is_empty() {
|
||||
// Add "Others will only see this group after you sent a first message." message.
|
||||
stock_str::new_group_send_first_message(context).await
|
||||
stock_str::new_group_send_first_message(context)
|
||||
} else {
|
||||
// Add "Messages in this chat use classic email and are not encrypted." message.
|
||||
stock_str::chat_unencrypted_explanation(context).await
|
||||
stock_str::chat_unencrypted_explanation(context)
|
||||
};
|
||||
add_info_msg(context, chat_id, &text).await?;
|
||||
}
|
||||
@@ -4168,7 +4181,7 @@ async fn send_member_removal_msg(
|
||||
|
||||
if contact_id == ContactId::SELF {
|
||||
if chat.typ == Chattype::InBroadcast {
|
||||
msg.text = stock_str::msg_you_left_broadcast(context).await;
|
||||
msg.text = stock_str::msg_you_left_broadcast(context);
|
||||
} else {
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
}
|
||||
@@ -4329,7 +4342,7 @@ async fn rename_ex(
|
||||
{
|
||||
msg.viewtype = Viewtype::Text;
|
||||
msg.text = if chat.typ == Chattype::OutBroadcast {
|
||||
stock_str::msg_broadcast_name_changed(context, &chat.name, &new_name).await
|
||||
stock_str::msg_broadcast_name_changed(context, &chat.name, &new_name)
|
||||
} else {
|
||||
stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await
|
||||
};
|
||||
@@ -4394,7 +4407,7 @@ pub async fn set_chat_profile_image(
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
msg.param.remove(Param::Arg);
|
||||
msg.text = if chat.typ == Chattype::OutBroadcast {
|
||||
stock_str::msg_broadcast_img_changed(context).await
|
||||
stock_str::msg_broadcast_img_changed(context)
|
||||
} else {
|
||||
stock_str::msg_grp_img_deleted(context, ContactId::SELF).await
|
||||
};
|
||||
@@ -4408,7 +4421,7 @@ pub async fn set_chat_profile_image(
|
||||
chat.param.set(Param::ProfileImage, image_blob.as_name());
|
||||
msg.param.set(Param::Arg, image_blob.as_name());
|
||||
msg.text = if chat.typ == Chattype::OutBroadcast {
|
||||
stock_str::msg_broadcast_img_changed(context).await
|
||||
stock_str::msg_broadcast_img_changed(context)
|
||||
} else {
|
||||
stock_str::msg_grp_img_changed(context, ContactId::SELF).await
|
||||
};
|
||||
@@ -4914,15 +4927,8 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
ts
|
||||
} else {
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
chat_id
|
||||
.calc_sort_timestamp(
|
||||
context,
|
||||
smeared_time(context),
|
||||
sort_to_bottom,
|
||||
received,
|
||||
incoming,
|
||||
)
|
||||
.calc_sort_timestamp(context, smeared_time(context), sort_to_bottom)
|
||||
.await?
|
||||
};
|
||||
|
||||
|
||||
@@ -806,7 +806,7 @@ async fn test_self_talk() -> Result<()> {
|
||||
assert!(chat.visibility == ChatVisibility::Normal);
|
||||
assert!(!chat.is_device_talk());
|
||||
assert!(chat.can_send(&t).await?);
|
||||
assert_eq!(chat.name, stock_str::saved_messages(&t).await);
|
||||
assert_eq!(chat.name, stock_str::saved_messages(&t));
|
||||
assert!(chat.get_profile_image(&t).await?.is_some());
|
||||
|
||||
let msg_id = send_text_msg(&t, chat.id, "foo self".to_string()).await?;
|
||||
@@ -866,7 +866,6 @@ async fn test_add_device_msg_unlabelled() {
|
||||
assert_eq!(msg1.from_id, ContactId::DEVICE);
|
||||
assert_eq!(msg1.to_id, ContactId::SELF);
|
||||
assert!(!msg1.is_info());
|
||||
assert!(!msg1.is_setupmessage());
|
||||
|
||||
let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await;
|
||||
assert!(msg2.is_ok());
|
||||
@@ -899,7 +898,6 @@ async fn test_add_device_msg_labelled() -> Result<()> {
|
||||
assert_eq!(msg1.from_id, ContactId::DEVICE);
|
||||
assert_eq!(msg1.to_id, ContactId::SELF);
|
||||
assert!(!msg1.is_info());
|
||||
assert!(!msg1.is_setupmessage());
|
||||
|
||||
// check device chat
|
||||
let chat_id = msg1.chat_id;
|
||||
@@ -913,7 +911,7 @@ async fn test_add_device_msg_labelled() -> Result<()> {
|
||||
assert!(!chat.can_send(&t).await?);
|
||||
assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat));
|
||||
|
||||
assert_eq!(chat.name, stock_str::device_messages(&t).await);
|
||||
assert_eq!(chat.name, stock_str::device_messages(&t));
|
||||
let device_msg_icon = chat.get_profile_image(&t).await?.unwrap();
|
||||
assert_eq!(
|
||||
device_msg_icon.metadata()?.len(),
|
||||
@@ -1331,6 +1329,54 @@ async fn test_marknoticed_all_chats() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markfresh_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// alice sends a message to Bob
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let sent_msg1 = alice.send_text(alice_chat.id, "hi bob!").await;
|
||||
|
||||
// bob received the message, fresh count is 1
|
||||
let bob_msg1 = bob.recv_msg(&sent_msg1).await;
|
||||
let bob_chat_id = bob_msg1.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
assert_eq!(bob_msg1.state, MessageState::InFresh);
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(bob).await?, 1);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
// alice sends another message to bob, fresh count is 2
|
||||
let sent_msg2 = alice.send_text(alice_chat.id, "howdy?").await;
|
||||
let bob_msg2 = bob.recv_msg(&sent_msg2).await;
|
||||
let bob_msg1 = Message::load_from_db(bob, bob_msg1.id).await?;
|
||||
assert_eq!(bob_msg1.state, MessageState::InFresh);
|
||||
assert_eq!(bob_msg2.state, MessageState::InFresh);
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(bob).await?, 2);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 2);
|
||||
|
||||
// bob marks the chat as noticed, messages are no longer fresh, fresh count is 0
|
||||
marknoticed_chat(bob, bob_chat_id).await?;
|
||||
let bob_msg1 = Message::load_from_db(bob, bob_msg1.id).await?;
|
||||
let bob_msg2 = Message::load_from_db(bob, bob_msg2.id).await?;
|
||||
assert_ne!(bob_msg1.state, MessageState::InFresh);
|
||||
assert_ne!(bob_msg2.state, MessageState::InFresh);
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(bob).await?, 0);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
// bob marks the chat as fresh again, fresh count is 1 again
|
||||
markfresh_chat(bob, bob_chat_id).await?;
|
||||
let bob_msg1 = Message::load_from_db(bob, bob_msg1.id).await?;
|
||||
let bob_msg2 = Message::load_from_db(bob, bob_msg2.id).await?;
|
||||
assert_ne!(bob_msg1.state, MessageState::InFresh);
|
||||
assert_eq!(bob_msg2.state, MessageState::InFresh);
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(bob).await?, 1);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archive_fresh_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -2746,7 +2792,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&vc_pubkey).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
assert!(parsed_by_bob.decryption_error.is_some());
|
||||
|
||||
charlie.recv_msg_trash(&vc_pubkey).await;
|
||||
}
|
||||
@@ -2775,7 +2821,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&member_added).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
assert!(parsed_by_bob.decryption_error.is_some());
|
||||
|
||||
let rcvd = charlie.recv_msg(&member_added).await;
|
||||
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberAddedToGroup);
|
||||
@@ -2790,7 +2836,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&hi_msg).await;
|
||||
assert_eq!(parsed_by_bob.decrypting_failed, false);
|
||||
assert!(parsed_by_bob.decryption_error.is_none());
|
||||
}
|
||||
|
||||
tcm.section("Alice removes Charlie. Bob must not see it.");
|
||||
@@ -2807,7 +2853,7 @@ async fn test_broadcast_members_cant_see_each_other() -> Result<()> {
|
||||
assert_eq!(parsed.decoded_data_contains("bob@example.net"), false);
|
||||
|
||||
let parsed_by_bob = bob.parse_msg(&member_removed).await;
|
||||
assert!(parsed_by_bob.decrypting_failed);
|
||||
assert!(parsed_by_bob.decryption_error.is_some());
|
||||
|
||||
let rcvd = charlie.recv_msg(&member_removed).await;
|
||||
assert_eq!(rcvd.param.get_cmd(), SystemMessage::MemberRemovedFromGroup);
|
||||
@@ -3289,7 +3335,12 @@ async fn test_chat_description(
|
||||
initial_description
|
||||
);
|
||||
|
||||
for description in ["This is a cool chat", "", "ä ẟ 😂"] {
|
||||
for description in [
|
||||
&"This<>is 'a' \"cool\" chat:/\\|?*".repeat(50),
|
||||
"multiple\nline\n\nbreaks\n\n\r\n.",
|
||||
"",
|
||||
"ä ẟ 😂",
|
||||
] {
|
||||
tcm.section(&format!(
|
||||
"Alice sets the chat description to '{description}'"
|
||||
));
|
||||
@@ -3746,7 +3797,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
assert_eq!(rcvd.chat_id, bob1_hello.chat_id);
|
||||
assert!(rcvd.is_info());
|
||||
assert_eq!(rcvd.get_info_type(), SystemMessage::MemberRemovedFromGroup);
|
||||
assert_eq!(rcvd.text, stock_str::msg_you_left_broadcast(bob1).await);
|
||||
assert_eq!(rcvd.text, stock_str::msg_you_left_broadcast(bob1));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4733,9 +4784,10 @@ async fn test_sync_broadcast_and_send_message() -> Result<()> {
|
||||
vec![a2b_contact_id]
|
||||
);
|
||||
|
||||
// alice2's smeared clock may be behind alice's one, so "hi" from alice2 may appear before "You
|
||||
// joined the channel." for bob.
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
// alice2's smeared clock may be behind alice1's one, so we need to work around "hi" appearing
|
||||
// before "You joined the channel." for bob. alice1 makes 3 more calls of
|
||||
// create_smeared_timestamp() than alice2 does as of 2026-03-10.
|
||||
SystemTime::shift(Duration::from_secs(3));
|
||||
tcm.section("Alice's second device sends a message to the channel");
|
||||
let sent_msg = alice2.send_text(a2_broadcast_id, "hi").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
@@ -6060,3 +6112,54 @@ async fn test_leftgrps() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if the message arrives late,
|
||||
/// it can still be sorted above the last seen message.
|
||||
///
|
||||
/// Versions 2.47 and below always sorted incoming messages
|
||||
/// after the last seen message, but with
|
||||
/// the introduction of multi-relay it is possible
|
||||
/// that some messages arrive only to some relays
|
||||
/// and are fetched after the already arrived seen message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_late_message_above_seen() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members("Group", &[bob, charlie])
|
||||
.await;
|
||||
let alice_sent = alice.send_text(alice_chat_id, "Hello everyone!").await;
|
||||
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
let charlie_chat_id = charlie.recv_msg(&alice_sent).await.chat_id;
|
||||
charlie_chat_id.accept(charlie).await?;
|
||||
|
||||
// Bob sends a message, but the message is delayed.
|
||||
let bob_sent = bob.send_text(bob_chat_id, "Hello from Bob!").await;
|
||||
SystemTime::shift(Duration::from_secs(1000));
|
||||
|
||||
let charlie_sent = charlie
|
||||
.send_text(charlie_chat_id, "Hello from Charlie!")
|
||||
.await;
|
||||
|
||||
// Alice immediately receives a message from Charlie and reads it.
|
||||
let alice_received_from_charlie = alice.recv_msg(&charlie_sent).await;
|
||||
assert_eq!(
|
||||
alice_received_from_charlie.get_text(),
|
||||
"Hello from Charlie!"
|
||||
);
|
||||
message::markseen_msgs(alice, vec![alice_received_from_charlie.id]).await?;
|
||||
|
||||
// Bob message arrives later, it should be above the message from Charlie.
|
||||
let alice_received_from_bob = alice.recv_msg(&bob_sent).await;
|
||||
assert_eq!(alice_received_from_bob.get_text(), "Hello from Bob!");
|
||||
|
||||
// The last message in the chat is still from Charlie.
|
||||
let last_msg = alice.get_last_msg_in(alice_chat_id).await;
|
||||
assert_eq!(last_msg.get_text(), "Hello from Charlie!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ impl Chatlist {
|
||||
AND c.blocked!=1
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
@@ -168,7 +168,7 @@ impl Chatlist {
|
||||
AND c.blocked!=1
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft,),
|
||||
process_row,
|
||||
)
|
||||
@@ -204,7 +204,7 @@ impl Chatlist {
|
||||
AND IFNULL(c.name_normalized,c.name) LIKE ?3
|
||||
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
process_row,
|
||||
)
|
||||
@@ -253,7 +253,7 @@ impl Chatlist {
|
||||
AND NOT c.archived=?
|
||||
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(
|
||||
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
|
||||
Chattype::Group, ContactId::SELF,
|
||||
@@ -279,7 +279,7 @@ impl Chatlist {
|
||||
AND (c.blocked=0 OR c.blocked=2)
|
||||
AND NOT c.archived=?
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(NULLIF(m.timestamp,0),c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
).await?
|
||||
@@ -417,7 +417,7 @@ impl Chatlist {
|
||||
Summary::new_with_reaction_details(context, &lastmsg, chat, lastcontact.as_ref()).await
|
||||
} else {
|
||||
Ok(Summary {
|
||||
text: stock_str::no_messages(context).await,
|
||||
text: stock_str::no_messages(context),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -648,7 +648,6 @@ mod tests {
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
t.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
|
||||
.await
|
||||
@@ -656,7 +655,6 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
t.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
|
||||
.await
|
||||
|
||||
@@ -837,7 +837,7 @@ impl Context {
|
||||
// which only fetches from the primary transport.
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE transports SET add_timestamp=? WHERE addr=?",
|
||||
"UPDATE transports SET add_timestamp=?, is_published=1 WHERE addr=?",
|
||||
(time(), addr),
|
||||
)
|
||||
.context(
|
||||
@@ -964,7 +964,22 @@ impl Context {
|
||||
pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports ORDER BY add_timestamp DESC",
|
||||
"SELECT addr FROM transports ORDER BY add_timestamp DESC, id DESC",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all published self addresses, newest first.
|
||||
/// See `[Context::set_transport_unpublished]`
|
||||
pub(crate) async fn get_published_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports WHERE is_published=1 ORDER BY add_timestamp DESC, id DESC",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
@@ -982,6 +997,24 @@ impl Context {
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Returns all published secondary self addresses.
|
||||
/// See `[Context::set_transport_unpublished]`
|
||||
pub(crate) async fn get_published_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports
|
||||
WHERE is_published
|
||||
AND addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')
|
||||
ORDER BY add_timestamp DESC, id DESC",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the primary self address.
|
||||
/// Returns an error if no self addr is configured.
|
||||
pub async fn get_primary_self_addr(&self) -> Result<String> {
|
||||
|
||||
@@ -246,8 +246,7 @@ async fn test_sync() -> Result<()> {
|
||||
alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.filter(|path| path.ends_with(".png"))
|
||||
.is_some()
|
||||
.is_some_and(|path| path.ends_with(".png"))
|
||||
);
|
||||
alice0.set_config(Config::Selfavatar, None).await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
@@ -305,16 +304,14 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.filter(|path| path.ends_with(".jpg"))
|
||||
.is_some()
|
||||
.is_some_and(|path| path.ends_with(".jpg"))
|
||||
);
|
||||
sync(alice1, alice0).await;
|
||||
assert!(
|
||||
alice0
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.filter(|path| path.ends_with(".jpg"))
|
||||
.is_some()
|
||||
.is_some_and(|path| path.ends_with(".jpg"))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -28,8 +28,8 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::warn;
|
||||
use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::login_param::{EnteredCertificateChecks, TransportListEntry};
|
||||
use crate::message::Message;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
@@ -110,6 +110,7 @@ impl Context {
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||
pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
let result = self.add_transport_inner(param).await;
|
||||
@@ -145,7 +146,7 @@ impl Context {
|
||||
if let Err(err) = res.as_ref() {
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}"));
|
||||
progress!(self, 0, Some(error_msg.clone()));
|
||||
bail!(error_msg);
|
||||
} else {
|
||||
@@ -188,14 +189,22 @@ impl Context {
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
|
||||
pub async fn list_transports(&self) -> Result<Vec<TransportListEntry>> {
|
||||
let transports = self
|
||||
.sql
|
||||
.query_map_vec("SELECT entered_param FROM transports", (), |row| {
|
||||
let entered_param: String = row.get(0)?;
|
||||
let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
|
||||
Ok(transport)
|
||||
})
|
||||
.query_map_vec(
|
||||
"SELECT entered_param, is_published FROM transports",
|
||||
(),
|
||||
|row| {
|
||||
let param: String = row.get(0)?;
|
||||
let param: EnteredLoginParam = serde_json::from_str(¶m)?;
|
||||
let is_published: bool = row.get(1)?;
|
||||
Ok(TransportListEntry {
|
||||
param,
|
||||
is_unpublished: !is_published,
|
||||
})
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(transports)
|
||||
@@ -234,11 +243,6 @@ impl Context {
|
||||
Ok((id, add_timestamp))
|
||||
},
|
||||
)?;
|
||||
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
|
||||
transaction.execute(
|
||||
"DELETE FROM imap_sync WHERE transport_id=?",
|
||||
(transport_id,),
|
||||
)?;
|
||||
|
||||
// Removal timestamp should not be lower than addition timestamp
|
||||
// to be accepted by other devices when synced.
|
||||
@@ -261,6 +265,44 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change whether the transport is unpublished.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// The default is false, but when the user updates from a version that didn't have this flag,
|
||||
/// existing secondary transports are set to unpublished,
|
||||
/// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
pub async fn set_transport_unpublished(&self, addr: &str, unpublished: bool) -> Result<()> {
|
||||
self.sql
|
||||
.transaction(|trans| {
|
||||
let primary_addr: String = trans
|
||||
.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| row.get(0),
|
||||
)
|
||||
.context("Select primary address")?;
|
||||
if primary_addr == addr && unpublished {
|
||||
bail!("Can't set primary relay as unpublished");
|
||||
}
|
||||
// We need to update the timestamp so that the key's timestamp changes
|
||||
// and is recognized as newer by our peers
|
||||
trans
|
||||
.execute(
|
||||
"UPDATE transports SET is_published=?, add_timestamp=? WHERE addr=? AND is_published!=?1",
|
||||
(!unpublished, time(), addr),
|
||||
)
|
||||
.context("Update transports")?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
@@ -590,10 +632,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
let imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(imap_session) => imap_session,
|
||||
Err(err) => {
|
||||
bail!(
|
||||
"{}",
|
||||
nicer_configuration_error(ctx, format!("{err:#}")).await
|
||||
);
|
||||
bail!("{}", nicer_configuration_error(ctx, format!("{err:#}")));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -734,7 +773,7 @@ async fn get_autoconfig(
|
||||
None
|
||||
}
|
||||
|
||||
async fn nicer_configuration_error(context: &Context, e: String) -> String {
|
||||
fn nicer_configuration_error(context: &Context, e: String) -> String {
|
||||
if e.to_lowercase().contains("could not resolve")
|
||||
|| e.to_lowercase().contains("connection attempts")
|
||||
|| e.to_lowercase()
|
||||
@@ -743,7 +782,7 @@ async fn nicer_configuration_error(context: &Context, e: String) -> String {
|
||||
|| e.to_lowercase()
|
||||
.contains("failed to lookup address information")
|
||||
{
|
||||
return stock_str::error_no_network(context).await;
|
||||
return stock_str::error_no_network(context);
|
||||
}
|
||||
|
||||
e
|
||||
|
||||
@@ -234,19 +234,6 @@ pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
|
||||
// Newer Delta Chats will remove the prefix as needed.
|
||||
pub(crate) const EDITED_PREFIX: &str = "✏️";
|
||||
|
||||
// Strings needed to render the Autocrypt Setup Message.
|
||||
// Left untranslated as not being supported/recommended workflow and as translations would require deep knowledge.
|
||||
pub(crate) const ASM_SUBJECT: &str = "Autocrypt Setup Message";
|
||||
pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
|
||||
used to transfer your end-to-end setup between clients.
|
||||
|
||||
To decrypt and use your setup, \
|
||||
open the message in an Autocrypt-compliant client \
|
||||
and enter the setup code presented on the generating device.
|
||||
|
||||
If you see this message in a chatmail client (Delta Chat, Arcane Chat, Delta Touch ...), \
|
||||
use \"Settings / Add Second Device\" instead.";
|
||||
|
||||
/// Period between `sql::housekeeping()` runs.
|
||||
pub(crate) const HOUSEKEEPING_PERIOD: i64 = 24 * 60 * 60;
|
||||
|
||||
|
||||
@@ -688,7 +688,7 @@ impl Contact {
|
||||
.await?
|
||||
{
|
||||
if contact_id == ContactId::SELF {
|
||||
contact.name = stock_str::self_msg(context).await;
|
||||
contact.name = stock_str::self_msg(context);
|
||||
contact.authname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
@@ -705,9 +705,9 @@ impl Contact {
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
} else if contact_id == ContactId::DEVICE {
|
||||
contact.name = stock_str::device_messages(context).await;
|
||||
contact.name = stock_str::device_messages(context);
|
||||
contact.addr = ContactId::DEVICE_ADDR.to_string();
|
||||
contact.status = stock_str::device_messages_hint(context).await;
|
||||
contact.status = stock_str::device_messages_hint(context);
|
||||
}
|
||||
Ok(Some(contact))
|
||||
} else {
|
||||
@@ -1240,7 +1240,7 @@ ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
|
||||
|
||||
if self_addr.contains(query)
|
||||
|| self_name.contains(query)
|
||||
|| self_name2.await.contains(query)
|
||||
|| self_name2.contains(query)
|
||||
{
|
||||
add_self = true;
|
||||
}
|
||||
@@ -1392,17 +1392,17 @@ WHERE addr=?
|
||||
.unwrap_or_default();
|
||||
|
||||
let Some(fingerprint_other) = contact.fingerprint() else {
|
||||
return Ok(stock_str::encr_none(context).await);
|
||||
return Ok(stock_str::encr_none(context));
|
||||
};
|
||||
let fingerprint_other = fingerprint_other.to_string();
|
||||
|
||||
let stock_message = if contact.public_key(context).await?.is_some() {
|
||||
stock_str::messages_are_e2ee(context).await
|
||||
stock_str::messages_are_e2ee(context)
|
||||
} else {
|
||||
stock_str::encr_none(context).await
|
||||
stock_str::encr_none(context)
|
||||
};
|
||||
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
let finger_prints = stock_str::finger_prints(context);
|
||||
let mut ret = format!("{stock_message}\n{finger_prints}:");
|
||||
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
@@ -1412,7 +1412,7 @@ WHERE addr=?
|
||||
if addr < contact.addr {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context).await,
|
||||
&stock_str::self_msg(context),
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
);
|
||||
@@ -1431,7 +1431,7 @@ WHERE addr=?
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context).await,
|
||||
&stock_str::self_msg(context),
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
);
|
||||
|
||||
@@ -282,7 +282,7 @@ async fn test_add_or_lookup() {
|
||||
|
||||
// check SELF
|
||||
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
|
||||
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
|
||||
assert_eq!(contact.get_name(), stock_str::self_msg(&t));
|
||||
assert_eq!(contact.get_addr(), "alice@example.org");
|
||||
assert!(!contact.is_blocked());
|
||||
}
|
||||
|
||||
@@ -266,15 +266,11 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
}
|
||||
}
|
||||
}
|
||||
"b" | "strong" => {
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
*dehtml.get_buf() += "*";
|
||||
}
|
||||
"b" | "strong" if dehtml.get_add_text() != AddText::No => {
|
||||
*dehtml.get_buf() += "*";
|
||||
}
|
||||
"i" | "em" => {
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
*dehtml.get_buf() += "_";
|
||||
}
|
||||
"i" | "em" if dehtml.get_add_text() != AddText::No => {
|
||||
*dehtml.get_buf() += "_";
|
||||
}
|
||||
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
|
||||
_ => {}
|
||||
@@ -341,15 +337,11 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
}
|
||||
}
|
||||
}
|
||||
"b" | "strong" => {
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
*dehtml.get_buf() += "*";
|
||||
}
|
||||
"b" | "strong" if dehtml.get_add_text() != AddText::No => {
|
||||
*dehtml.get_buf() += "*";
|
||||
}
|
||||
"i" | "em" => {
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
*dehtml.get_buf() += "_";
|
||||
}
|
||||
"i" | "em" if dehtml.get_add_text() != AddText::No => {
|
||||
*dehtml.get_buf() += "_";
|
||||
}
|
||||
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
|
||||
_ => {}
|
||||
|
||||
@@ -165,6 +165,7 @@ pub(crate) async fn download_msg(
|
||||
|
||||
let Some((server_uid, server_folder, msg_transport_id)) = row else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
delete_from_available_post_msgs(context, &rfc724_mid).await?;
|
||||
return Err(anyhow!(
|
||||
"IMAP location for {rfc724_mid:?} post-message is unknown"
|
||||
));
|
||||
@@ -326,22 +327,25 @@ pub(crate) async fn download_known_post_messages_without_pre_message(
|
||||
})
|
||||
.await?;
|
||||
for rfc724_mid in &rfc724_mids {
|
||||
if !msg_is_downloaded_for(context, rfc724_mid).await? {
|
||||
// Download the Post-Message unconditionally,
|
||||
// because the Pre-Message got lost.
|
||||
// The message may be in the wrong order,
|
||||
// but at least we have it at all.
|
||||
let res = download_msg(context, rfc724_mid.clone(), session).await;
|
||||
if let Ok(Some(())) = res {
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
}
|
||||
if let Err(err) = res {
|
||||
warn!(
|
||||
context,
|
||||
"download_known_post_messages_without_pre_message: Failed to download message rfc724_mid={rfc724_mid}: {:#}.",
|
||||
err
|
||||
);
|
||||
}
|
||||
if msg_is_downloaded_for(context, rfc724_mid).await? {
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download the Post-Message unconditionally,
|
||||
// because the Pre-Message got lost.
|
||||
// The message may be in the wrong order,
|
||||
// but at least we have it at all.
|
||||
let res = download_msg(context, rfc724_mid.clone(), session).await;
|
||||
if let Ok(Some(())) = res {
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
}
|
||||
if let Err(err) = res {
|
||||
warn!(
|
||||
context,
|
||||
"download_known_post_messages_without_pre_message: Failed to download message rfc724_mid={rfc724_mid}: {:#}.",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
12
src/e2ee.rs
12
src/e2ee.rs
@@ -40,7 +40,6 @@ impl EncryptHelper {
|
||||
keyring: Vec<SignedPublicKey>,
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
compress: bool,
|
||||
anonymous_recipients: bool,
|
||||
seipd_version: SeipdVersion,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
@@ -49,15 +48,8 @@ impl EncryptHelper {
|
||||
let cursor = Cursor::new(&mut raw_message);
|
||||
mail_to_encrypt.clone().write_part(cursor).ok();
|
||||
|
||||
let ctext = pgp::pk_encrypt(
|
||||
raw_message,
|
||||
keyring,
|
||||
sign_key,
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
seipd_version,
|
||||
)
|
||||
.await?;
|
||||
let ctext =
|
||||
pgp::pk_encrypt(raw_message, keyring, sign_key, compress, seipd_version).await?;
|
||||
|
||||
Ok(ctext)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub enum EventType {
|
||||
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
///
|
||||
/// However, for ongoing processes (eg. configure())
|
||||
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
|
||||
/// or for functions that are expected to fail
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a message box then.
|
||||
@@ -397,6 +397,8 @@ pub enum EventType {
|
||||
msg_id: MsgId,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
/// The call was accepted from this device (process).
|
||||
from_this_device: bool,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
|
||||
@@ -125,7 +125,6 @@ pub enum HeaderDef {
|
||||
/// [Autocrypt](https://autocrypt.org/) header.
|
||||
Autocrypt,
|
||||
AutocryptGossip,
|
||||
AutocryptSetupMessage,
|
||||
SecureJoin,
|
||||
|
||||
/// Deprecated header containing Group-ID in `vg-request-with-auth` message.
|
||||
@@ -208,10 +207,10 @@ mod tests {
|
||||
/// Test that headers are parsed case-insensitively
|
||||
fn test_get_header_value_case() {
|
||||
let (headers, _) =
|
||||
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
|
||||
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-GoSsIp: fooBaR").unwrap();
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
|
||||
Some("v99".to_string())
|
||||
headers.get_header_value(HeaderDef::AutocryptGossip),
|
||||
Some("fooBaR".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::From_),
|
||||
|
||||
72
src/imap.rs
72
src/imap.rs
@@ -296,6 +296,11 @@ impl Imap {
|
||||
Ok(imap)
|
||||
}
|
||||
|
||||
/// Returns transport ID of the IMAP client.
|
||||
pub fn transport_id(&self) -> u32 {
|
||||
self.transport_id
|
||||
}
|
||||
|
||||
/// Connects to IMAP server and returns a new IMAP session.
|
||||
///
|
||||
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
|
||||
@@ -437,7 +442,7 @@ impl Imap {
|
||||
|
||||
Err(err) => {
|
||||
let imap_user = lp.user.to_owned();
|
||||
let message = stock_str::cannot_login(context, &imap_user).await;
|
||||
let message = stock_str::cannot_login(context, &imap_user);
|
||||
|
||||
warn!(context, "IMAP failed to login: {err:#}.");
|
||||
first_error.get_or_insert(format_err!("{message} ({err:#})"));
|
||||
@@ -551,8 +556,13 @@ impl Imap {
|
||||
folder: &str,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<bool> {
|
||||
let transport_id = session.transport_id();
|
||||
|
||||
if should_ignore_folder(context, folder, folder_meaning).await? {
|
||||
info!(context, "Not fetching from {folder:?}.");
|
||||
info!(
|
||||
context,
|
||||
"Transport {transport_id}: Not fetching from {folder:?}."
|
||||
);
|
||||
session.new_mail = false;
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -561,16 +571,22 @@ impl Imap {
|
||||
.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("Failed to select folder {folder:?}"))?;
|
||||
|
||||
if !session.new_mail {
|
||||
info!(
|
||||
context,
|
||||
"Transport {transport_id}: No new emails in folder {folder:?}."
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
// Make sure not to return before setting new_mail to false
|
||||
// Otherwise, we will skip IDLE and go into an infinite loop
|
||||
session.new_mail = false;
|
||||
|
||||
if !folder_exists {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !session.new_mail {
|
||||
info!(context, "No new emails in folder {folder:?}.");
|
||||
return Ok(false);
|
||||
}
|
||||
session.new_mail = false;
|
||||
|
||||
let mut read_cnt = 0;
|
||||
loop {
|
||||
let (n, fetch_more) = self
|
||||
@@ -1087,15 +1103,22 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap_markseen WHERE id NOT IN (SELECT imap.id FROM imap)",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let transport_id = self.transport_id();
|
||||
let rows = context
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT imap.id, uid, folder FROM imap, imap_markseen
|
||||
WHERE imap.id = imap_markseen.id
|
||||
AND imap.transport_id=?
|
||||
AND target = folder
|
||||
ORDER BY folder, uid",
|
||||
AND target = folder",
|
||||
(transport_id,),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
@@ -1106,6 +1129,16 @@ impl Session {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Number of SQL results is expected to be low as
|
||||
// we usually don't have many messages to mark on IMAP at once.
|
||||
// We are sorting outside of SQL to avoid SQLite constructing a query plan
|
||||
// that scans the whole `imap` table. Scanning `imap_markseen` is fine
|
||||
// as it should not have many items.
|
||||
// If you change the SQL query, test it with `EXPLAIN QUERY PLAN`.
|
||||
rows.sort_unstable_by(|(_rowid1, uid1, folder1), (_rowid2, uid2, folder2)| {
|
||||
(folder1, uid1).cmp(&(folder2, uid2))
|
||||
});
|
||||
|
||||
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
|
||||
Err(err) => {
|
||||
@@ -1237,6 +1270,7 @@ impl Session {
|
||||
// have been modified while our request was in progress.
|
||||
// We may or may not have these new flags as a part of the response,
|
||||
// so better skip next IDLE and do another round of flag synchronization.
|
||||
info!(context, "Got unsolicited fetch, will skip idle");
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
@@ -1562,11 +1596,18 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
let Some(device_token) = context.push_subscriber.device_token().await else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if self.can_metadata() && self.can_push() {
|
||||
info!(
|
||||
context,
|
||||
"Transport {transport_id}: Subscribing for push notifications."
|
||||
);
|
||||
|
||||
let old_encrypted_device_token =
|
||||
context.get_config(Config::EncryptedDeviceToken).await?;
|
||||
|
||||
@@ -1974,15 +2015,6 @@ async fn needs_move_to_mvbox(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if headers
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.is_some()
|
||||
{
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if has_chat_version {
|
||||
Ok(true)
|
||||
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
|
||||
|
||||
@@ -27,6 +27,8 @@ impl Session {
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
folder: &str,
|
||||
) -> Result<Self> {
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
@@ -36,13 +38,16 @@ impl Session {
|
||||
if self.new_mail {
|
||||
info!(
|
||||
context,
|
||||
"Skipping IDLE in {folder:?} because there may be new mail."
|
||||
"Transport {transport_id}: Skipping IDLE in {folder:?} because there may be new mail."
|
||||
);
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
if let Ok(()) = idle_interrupt_receiver.try_recv() {
|
||||
info!(context, "Skip IDLE in {folder:?} because we got interrupt.");
|
||||
info!(
|
||||
context,
|
||||
"Transport {transport_id}: Skip IDLE in {folder:?} because we got interrupt."
|
||||
);
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
@@ -61,7 +66,7 @@ impl Session {
|
||||
|
||||
info!(
|
||||
context,
|
||||
"IDLE entering wait-on-remote state in folder {folder:?}."
|
||||
"Transport {transport_id}: IDLE entering wait-on-remote state in folder {folder:?}."
|
||||
);
|
||||
|
||||
// Spawn a task to relay interrupts from `idle_interrupt_receiver`
|
||||
|
||||
@@ -105,10 +105,9 @@ async fn check_target_folder_combination(
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"
|
||||
"Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}"
|
||||
);
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -125,9 +124,7 @@ async fn check_target_folder_combination(
|
||||
}
|
||||
let temp;
|
||||
|
||||
let bytes = if setupmessage {
|
||||
include_bytes!("../../test-data/message/AutocryptSetupMessage.eml")
|
||||
} else {
|
||||
let bytes = {
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
@@ -164,7 +161,7 @@ async fn check_target_folder_combination(
|
||||
assert_eq!(
|
||||
expected,
|
||||
actual.as_deref(),
|
||||
"For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"
|
||||
"For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}: expected {expected:?}, got {actual:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -204,7 +201,6 @@ async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -221,7 +217,6 @@ async fn test_target_folder_incoming_request() -> Result<()> {
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -239,25 +234,6 @@ async fn test_target_folder_outgoing() -> Result<()> {
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -71,13 +71,18 @@ impl ImapSession {
|
||||
self.select(folder).await
|
||||
};
|
||||
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
info!(context, "Selected folder {folder:?}.");
|
||||
info!(
|
||||
context,
|
||||
"Transport {transport_id}: Selected folder {folder:?}."
|
||||
);
|
||||
self.selected_folder = Some(folder.to_string());
|
||||
self.selected_mailbox = Some(mailbox);
|
||||
Ok(NewlySelected::Yes)
|
||||
|
||||
14
src/imex.rs
14
src/imex.rs
@@ -28,11 +28,9 @@ use crate::tools::{
|
||||
write_file,
|
||||
};
|
||||
|
||||
mod key_transfer;
|
||||
mod transfer;
|
||||
|
||||
use ::pgp::types::KeyDetails;
|
||||
pub use key_transfer::{continue_key_transfer, initiate_key_transfer};
|
||||
pub use transfer::{BackupProvider, get_backup};
|
||||
|
||||
// Name of the database file in the backup.
|
||||
@@ -1139,4 +1137,16 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests importing a backup from Delta Chat 1.30.3 for Android (core v1.86.0).
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_import_ancient_backup() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context = &tcm.unconfigured().await;
|
||||
|
||||
let backup_path = Path::new("test-data/core-1.86.0-backup.tar");
|
||||
imex(context, ImexMode::ImportBackup, backup_path, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
//! # Key transfer via Autocrypt Setup Message.
|
||||
use std::io::BufReader;
|
||||
|
||||
use anyhow::{Result, bail, ensure};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{ASM_BODY, ASM_SUBJECT};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::imex::set_self_key;
|
||||
use crate::key::{DcKey, load_self_secret_key};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::pgp;
|
||||
use crate::tools::open_file_std;
|
||||
|
||||
/// Initiates key transfer via Autocrypt Setup Message.
|
||||
///
|
||||
/// Returns setup code.
|
||||
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
let setup_code = create_setup_code(context);
|
||||
/* this may require a keypair to be created. this may take a second ... */
|
||||
let setup_file_content = render_setup_file(context, &setup_code).await?;
|
||||
/* encrypting may also take a while ... */
|
||||
let setup_file_blob = BlobObject::create_and_deduplicate_from_bytes(
|
||||
context,
|
||||
setup_file_content.as_bytes(),
|
||||
"autocrypt-setup-message.html",
|
||||
)?;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
msg.param
|
||||
.set(Param::Filename, "autocrypt-setup-message.html");
|
||||
msg.subject = ASM_SUBJECT.to_owned();
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
|
||||
// Enable BCC-self, because transferring a key
|
||||
// means we have a multi-device setup.
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
Ok(setup_code)
|
||||
}
|
||||
|
||||
/// Continue key transfer via Autocrypt Setup Message.
|
||||
///
|
||||
/// `msg_id` is the ID of the received Autocrypt Setup Message.
|
||||
/// `setup_code` is the code entered by the user.
|
||||
pub async fn continue_key_transfer(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
setup_code: &str,
|
||||
) -> Result<()> {
|
||||
ensure!(!msg_id.is_special(), "wrong id");
|
||||
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
ensure!(
|
||||
msg.is_setupmessage(),
|
||||
"Message is no Autocrypt Setup Message."
|
||||
);
|
||||
|
||||
if let Some(filename) = msg.get_file(context) {
|
||||
let file = open_file_std(context, filename)?;
|
||||
let sc = normalize_setup_code(setup_code);
|
||||
let armored_key = decrypt_setup_file(&sc, BufReader::new(file)).await?;
|
||||
set_self_key(context, &armored_key).await?;
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Message is no Autocrypt Setup Message.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders HTML body of a setup file message.
|
||||
///
|
||||
/// The `passphrase` must be at least 2 characters long.
|
||||
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
|
||||
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
|
||||
passphrase_begin
|
||||
} else {
|
||||
bail!("Passphrase must be at least 2 chars long.");
|
||||
};
|
||||
let private_key = load_self_secret_key(context).await?;
|
||||
let ac_headers = Some(("Autocrypt-Prefer-Encrypt", "mutual"));
|
||||
let private_key_asc = private_key.to_asc(ac_headers);
|
||||
let encr = pgp::symm_encrypt_autocrypt_setup(passphrase, private_key_asc.into_bytes())
|
||||
.await?
|
||||
.replace('\n', "\r\n");
|
||||
|
||||
let replacement = format!(
|
||||
concat!(
|
||||
"-----BEGIN PGP MESSAGE-----\r\n",
|
||||
"Passphrase-Format: numeric9x4\r\n",
|
||||
"Passphrase-Begin: {}"
|
||||
),
|
||||
passphrase_begin
|
||||
);
|
||||
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
|
||||
|
||||
let msg_subj = ASM_SUBJECT;
|
||||
let msg_body = ASM_BODY.to_string();
|
||||
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
|
||||
Ok(format!(
|
||||
concat!(
|
||||
"<!DOCTYPE html>\r\n",
|
||||
"<html>\r\n",
|
||||
" <head>\r\n",
|
||||
" <title>{}</title>\r\n",
|
||||
" </head>\r\n",
|
||||
" <body>\r\n",
|
||||
" <h1>{}</h1>\r\n",
|
||||
" <p>{}</p>\r\n",
|
||||
" <pre>\r\n{}\r\n</pre>\r\n",
|
||||
" </body>\r\n",
|
||||
"</html>\r\n"
|
||||
),
|
||||
msg_subj, msg_subj, msg_body_html, pgp_msg
|
||||
))
|
||||
}
|
||||
|
||||
/// Creates a new setup code for Autocrypt Setup Message.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn create_setup_code(_context: &Context) -> String {
|
||||
let mut random_val: u16;
|
||||
let mut ret = String::new();
|
||||
|
||||
for i in 0..9 {
|
||||
loop {
|
||||
random_val = rand::random();
|
||||
if random_val as usize <= 60000 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
random_val = (random_val as usize % 10000) as u16;
|
||||
ret += &format!(
|
||||
"{}{:04}",
|
||||
if 0 != i { "-" } else { "" },
|
||||
random_val as usize
|
||||
);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
async fn decrypt_setup_file<T: std::fmt::Debug + std::io::BufRead + Send + 'static>(
|
||||
passphrase: &str,
|
||||
file: T,
|
||||
) -> Result<String> {
|
||||
let plain_bytes = pgp::symm_decrypt(passphrase, file).await?;
|
||||
let plain_text = std::string::String::from_utf8(plain_bytes)?;
|
||||
|
||||
Ok(plain_text)
|
||||
}
|
||||
|
||||
fn normalize_setup_code(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for c in s.chars() {
|
||||
if c.is_ascii_digit() {
|
||||
out.push(c);
|
||||
if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() {
|
||||
out += "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::pgp::{HEADER_AUTOCRYPT, HEADER_SETUPCODE, split_armored_data};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use ::pgp::armor::BlockType;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_setup_file() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let msg = render_setup_file(&t, "hello").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
// Check some substrings, indicating things got substituted.
|
||||
assert!(msg.contains("<title>Autocrypt Setup Message</title"));
|
||||
assert!(msg.contains("<h1>Autocrypt Setup Message</h1>"));
|
||||
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to"));
|
||||
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
|
||||
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
|
||||
assert!(msg.contains("Passphrase-Begin: he\r\n"));
|
||||
assert!(msg.contains("-----END PGP MESSAGE-----\r\n"));
|
||||
|
||||
for line in msg.rsplit_terminator('\n') {
|
||||
assert!(line.ends_with('\r'));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_setup_file_newline_replace() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let msg = render_setup_file(&t, "pw").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to transfer your end-to-end setup between clients.<br>"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_setup_code() {
|
||||
let t = TestContext::new().await;
|
||||
let setupcode = create_setup_code(&t);
|
||||
assert_eq!(setupcode.len(), 44);
|
||||
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(14).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(19).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(24).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(29).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(34).unwrap(), '-');
|
||||
assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_setup_code() {
|
||||
let norm = normalize_setup_code("123422343234423452346234723482349234");
|
||||
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
|
||||
|
||||
let norm =
|
||||
normalize_setup_code("\t1 2 3422343234- foo bar-- 423-45 2 34 6234723482349234 ");
|
||||
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
|
||||
}
|
||||
|
||||
/* S_EM_SETUPFILE is a AES-256 symm. encrypted setup message created by Enigmail
|
||||
with an "encrypted session key", see RFC 4880. The code is in S_EM_SETUPCODE */
|
||||
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
|
||||
const S_EM_SETUPFILE: &str = include_str!("../../test-data/message/stress.txt");
|
||||
|
||||
// Autocrypt Setup Message payload "encrypted" with plaintext algorithm.
|
||||
const S_PLAINTEXT_SETUPFILE: &str =
|
||||
include_str!("../../test-data/message/plaintext-autocrypt-setup.txt");
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_split_and_decrypt() {
|
||||
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
|
||||
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
|
||||
assert_eq!(typ, BlockType::Message);
|
||||
assert!(S_EM_SETUPCODE.starts_with(headers.get(HEADER_SETUPCODE).unwrap()));
|
||||
assert!(!headers.contains_key(HEADER_AUTOCRYPT));
|
||||
|
||||
assert!(!base64.is_empty());
|
||||
|
||||
let setup_file = S_EM_SETUPFILE;
|
||||
let decrypted = decrypt_setup_file(S_EM_SETUPCODE, setup_file.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
|
||||
|
||||
assert_eq!(typ, BlockType::PrivateKey);
|
||||
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
|
||||
assert!(!headers.contains_key(HEADER_SETUPCODE));
|
||||
}
|
||||
|
||||
/// Tests that Autocrypt Setup Message encrypted with "plaintext" algorithm cannot be
|
||||
/// decrypted.
|
||||
///
|
||||
/// According to <https://datatracker.ietf.org/doc/html/rfc4880#section-13.4>
|
||||
/// "Implementations MUST NOT use plaintext in Symmetrically Encrypted Data packets".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_plaintext_autocrypt_setup_message() {
|
||||
let setup_file = S_PLAINTEXT_SETUPFILE;
|
||||
let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
|
||||
assert!(
|
||||
decrypt_setup_file(incorrect_setupcode, setup_file.as_bytes(),)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_key_transfer() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
tcm.section("Alice sends Autocrypt setup message");
|
||||
alice.set_config(Config::BccSelf, Some("0")).await?;
|
||||
let setup_code = initiate_key_transfer(alice).await?;
|
||||
|
||||
// Test that sending Autocrypt Setup Message enables `bcc_self`.
|
||||
assert_eq!(alice.get_config_bool(Config::BccSelf).await?, true);
|
||||
|
||||
// Get Autocrypt Setup Message.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
tcm.section("Alice sets up a second device");
|
||||
let alice2 = &tcm.unconfigured().await;
|
||||
alice2.set_name("alice2");
|
||||
alice2.configure_addr("alice@example.org").await;
|
||||
alice2.recv_msg(&sent).await;
|
||||
let msg = alice2.get_last_msg().await;
|
||||
assert!(msg.is_setupmessage());
|
||||
assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 0);
|
||||
|
||||
// Transfer the key.
|
||||
tcm.section("Alice imports a key from Autocrypt Setup Message");
|
||||
alice2.set_config(Config::BccSelf, Some("0")).await?;
|
||||
continue_key_transfer(alice2, msg.id, &setup_code).await?;
|
||||
assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 1);
|
||||
|
||||
// Alice sends a message to self from the new device.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
let rcvd_msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(rcvd_msg.get_text(), "Test");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that Autocrypt Setup Messages is only clickable if it is self-sent.
|
||||
/// This prevents Bob from tricking Alice into changing the key
|
||||
/// by sending her an Autocrypt Setup Message as long as Alice's server
|
||||
/// does not allow to forge the `From:` header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_key_transfer_non_self_sent() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let _setup_code = initiate_key_transfer(&alice).await?;
|
||||
|
||||
// Get Autocrypt Setup Message.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(!rcvd.is_setupmessage());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests reception of Autocrypt Setup Message from K-9 6.802.
|
||||
///
|
||||
/// Unlike Autocrypt Setup Message sent by Delta Chat,
|
||||
/// this message does not contain `Autocrypt-Prefer-Encrypt` header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_key_transfer_k_9() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
t.configure_addr("autocrypt@nine.testrun.org").await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/k-9-autocrypt-setup-message.eml");
|
||||
let received = receive_imf(t, raw, false).await?.unwrap();
|
||||
|
||||
let setup_code = "0655-9868-8252-5455-4232-5158-1237-5333-2638";
|
||||
continue_key_transfer(t, *received.msg_ids.last().unwrap(), setup_code).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -208,7 +208,7 @@ impl BackupProvider {
|
||||
info!(context, "Received backup reception acknowledgement.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(&context).await);
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(&context));
|
||||
add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -296,7 +296,7 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
|
||||
.await?
|
||||
.context("No transports configured")?;
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let all_addrs = context.get_all_self_addrs().await?.join(",");
|
||||
let all_addrs = context.get_published_self_addrs().await?.join(",");
|
||||
let signed_public_key =
|
||||
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
|
||||
*lock = Some(signed_public_key.clone());
|
||||
@@ -756,8 +756,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
/// this resulted in various number of garbage
|
||||
/// octets at the end of the key, starting from 3 octets,
|
||||
/// but possibly 4 or 5 and maybe more octets
|
||||
/// if the key is imported or transferred
|
||||
/// using Autocrypt Setup Message multiple times.
|
||||
/// if the key is imported multiple times.
|
||||
#[test]
|
||||
fn test_ignore_trailing_garbage() {
|
||||
// Test several variants of garbage.
|
||||
|
||||
@@ -288,13 +288,13 @@ pub async fn send_locations_to_chat(
|
||||
)
|
||||
.await?;
|
||||
if 0 != seconds && !is_sending_locations_before {
|
||||
let mut msg = Message::new_text(stock_str::msg_location_enabled(context).await);
|
||||
let mut msg = Message::new_text(stock_str::msg_location_enabled(context));
|
||||
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
let stock_str = stock_str::msg_location_disabled(context);
|
||||
chat::add_info_msg(context, chat_id, &stock_str).await?;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
@@ -852,7 +852,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
.await
|
||||
.context("failed to disable location streaming")?;
|
||||
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
let stock_str = stock_str::msg_location_disabled(context);
|
||||
chat::add_info_msg(context, chat_id, &stock_str).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
@@ -79,6 +79,16 @@ pub struct EnteredServerLoginParam {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// A transport, as shown in the "relays" list in the UI.
|
||||
#[derive(Debug)]
|
||||
pub struct TransportListEntry {
|
||||
/// The login data entered by the user.
|
||||
pub param: EnteredLoginParam,
|
||||
/// Whether this transport is set to 'unpublished'.
|
||||
/// See [`Context::set_transport_unpublished`] for details.
|
||||
pub is_unpublished: bool,
|
||||
}
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EnteredLoginParam {
|
||||
|
||||
@@ -30,7 +30,6 @@ use crate::location::delete_poi_location;
|
||||
use crate::log::warn;
|
||||
use crate::mimeparser::{SystemMessage, parse_message_id};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::reaction::get_msg_reactions;
|
||||
use crate::sql;
|
||||
use crate::summary::Summary;
|
||||
@@ -599,7 +598,7 @@ impl Message {
|
||||
|
||||
if let Some(msg) = &mut msg {
|
||||
msg.additional_text =
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param).await?;
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param)?;
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
@@ -608,7 +607,7 @@ impl Message {
|
||||
/// Returns additional text which is appended to the message's text field
|
||||
/// when it is loaded from the database.
|
||||
/// Currently this is used to add infomation to pre-messages of what the download will be and how large it is
|
||||
async fn get_additional_text(
|
||||
fn get_additional_text(
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
param: &Params,
|
||||
@@ -631,7 +630,7 @@ impl Message {
|
||||
return match viewtype {
|
||||
Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
|
||||
_ => {
|
||||
let translated_viewtype = viewtype.to_locale_string(context).await;
|
||||
let translated_viewtype = viewtype.to_locale_string(context);
|
||||
Ok(format!(" [{translated_viewtype} – {file_size}]"))
|
||||
}
|
||||
};
|
||||
@@ -816,7 +815,11 @@ impl Message {
|
||||
|
||||
/// Returns the timestamp of the message for sorting.
|
||||
pub fn get_sort_timestamp(&self) -> i64 {
|
||||
self.timestamp_sort
|
||||
if self.timestamp_sort != 0 {
|
||||
self.timestamp_sort
|
||||
} else {
|
||||
self.timestamp_sent
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the text of the message.
|
||||
@@ -1059,34 +1062,6 @@ impl Message {
|
||||
cmd != SystemMessage::Unknown
|
||||
}
|
||||
|
||||
/// Returns true if the message is an Autocrypt Setup Message.
|
||||
pub fn is_setupmessage(&self) -> bool {
|
||||
if self.viewtype != Viewtype::File {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.param.get_cmd() == SystemMessage::AutocryptSetupMessage
|
||||
}
|
||||
|
||||
/// Returns the first characters of the setup code.
|
||||
///
|
||||
/// This is used to pre-fill the first entry field of the setup code.
|
||||
pub async fn get_setupcodebegin(&self, context: &Context) -> Option<String> {
|
||||
if !self.is_setupmessage() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(filename) = self.get_file(context)
|
||||
&& let Ok(ref buf) = read_file(context, &filename).await
|
||||
&& let Ok((typ, headers, _)) = split_armored_data(buf)
|
||||
&& typ == pgp::armor::BlockType::Message
|
||||
{
|
||||
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Sets or unsets message text.
|
||||
pub fn set_text(&mut self, text: String) {
|
||||
self.text = text;
|
||||
@@ -1934,10 +1909,22 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
// We also don't send read receipts for contact requests.
|
||||
// Read receipts will not be sent even after accepting the chat.
|
||||
let to_id = if curr_blocked == Blocked::Not
|
||||
&& !curr_hidden
|
||||
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& curr_param.get_cmd() == SystemMessage::Unknown
|
||||
&& context.should_send_mdns().await?
|
||||
{
|
||||
// Clear WantsMdn to not handle a MDN twice
|
||||
// if the state later is InFresh again as markfresh_chat() was called.
|
||||
// BccSelf MDN messages in the next branch may be sent twice for syncing.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET param=? WHERE id=?",
|
||||
(curr_param.clone().remove(Param::WantsMdn).to_string(), id),
|
||||
)
|
||||
.await
|
||||
.context("failed to clear WantsMdn")?;
|
||||
Some(curr_from_id)
|
||||
} else if context.get_config_bool(Config::BccSelf).await? {
|
||||
Some(ContactId::SELF)
|
||||
@@ -1955,6 +1942,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
|
||||
if !curr_hidden {
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat, PARAM_BROADCAST_SECRET, load_broadcast_secret};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{ASM_SUBJECT, BROADCAST_INCOMPATIBILITY_MSG};
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::constants::{BROADCAST_INCOMPATIBILITY_MSG, Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::download::PostMsgMetadata;
|
||||
@@ -33,7 +32,7 @@ use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{SystemMessage, is_hidden};
|
||||
use crate::param::Param;
|
||||
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
|
||||
use crate::pgp::{SeipdVersion, addresses_from_public_key};
|
||||
use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2};
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
@@ -733,7 +732,7 @@ impl MimeFactory {
|
||||
Some(name) => name,
|
||||
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
|
||||
};
|
||||
stock_str::subject_for_new_contact(context, self_name).await
|
||||
stock_str::subject_for_new_contact(context, self_name)
|
||||
}
|
||||
Loaded::Mdn { .. } => "Receipt Notification".to_string(), // untranslated to no reveal sender's language
|
||||
};
|
||||
@@ -853,7 +852,13 @@ impl MimeFactory {
|
||||
|
||||
let rfc724_mid = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => match &self.pre_message_mode {
|
||||
PreMessageMode::Pre { .. } => create_outgoing_rfc724_mid(),
|
||||
PreMessageMode::Pre { .. } => {
|
||||
if msg.pre_rfc724_mid.is_empty() {
|
||||
create_outgoing_rfc724_mid()
|
||||
} else {
|
||||
msg.pre_rfc724_mid.clone()
|
||||
}
|
||||
}
|
||||
_ => msg.rfc724_mid.clone(),
|
||||
},
|
||||
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
|
||||
@@ -1163,17 +1168,6 @@ impl MimeFactory {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Do not anonymize OpenPGP recipients.
|
||||
//
|
||||
// This is disabled to avoid interoperability problems
|
||||
// with old core versions <1.160.0 that do not support
|
||||
// receiving messages with wildcard Key IDs:
|
||||
// <https://github.com/chatmail/core/issues/7378>
|
||||
//
|
||||
// The option should be changed to true
|
||||
// once new core versions are sufficiently deployed.
|
||||
let anonymous_recipients = false;
|
||||
|
||||
if context.get_config_bool(Config::TestHooks).await?
|
||||
&& let Some(hook) = &*context.pre_encrypt_mime_hook.lock()
|
||||
{
|
||||
@@ -1188,14 +1182,13 @@ impl MimeFactory {
|
||||
} else {
|
||||
// Asymmetric encryption
|
||||
|
||||
let seipd_version = if encryption_pubkeys.is_empty() {
|
||||
// If message is sent only to self,
|
||||
// use v2 SEIPD.
|
||||
// Use SEIPDv2 if all recipients support it.
|
||||
let seipd_version = if encryption_pubkeys
|
||||
.iter()
|
||||
.all(|(_addr, pubkey)| pubkey_supports_seipdv2(pubkey))
|
||||
{
|
||||
SeipdVersion::V2
|
||||
} else {
|
||||
// If message is sent to others,
|
||||
// they may not support v2 SEIPD yet,
|
||||
// so use v1 SEIPD.
|
||||
SeipdVersion::V1
|
||||
};
|
||||
|
||||
@@ -1211,7 +1204,6 @@ impl MimeFactory {
|
||||
encryption_keyring,
|
||||
message,
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
seipd_version,
|
||||
)
|
||||
.await?
|
||||
@@ -1538,7 +1530,7 @@ impl MimeFactory {
|
||||
let description = chat::get_chat_description(context, chat.id).await?;
|
||||
headers.push((
|
||||
"Chat-Group-Description",
|
||||
mail_builder::headers::text::Text::new(description.clone()).into(),
|
||||
mail_builder::headers::raw::Raw::new(b_encode(&description)).into(),
|
||||
));
|
||||
if let Some(ts) = chat.param.get_i64(Param::GroupDescriptionTimestamp) {
|
||||
headers.push((
|
||||
@@ -1575,14 +1567,6 @@ impl MimeFactory {
|
||||
mail_builder::headers::raw::Raw::new("auto-generated").into(),
|
||||
));
|
||||
}
|
||||
SystemMessage::AutocryptSetupMessage => {
|
||||
headers.push((
|
||||
"Autocrypt-Setup-Message",
|
||||
mail_builder::headers::raw::Raw::new("v1").into(),
|
||||
));
|
||||
|
||||
placeholdertext = Some(ASM_SUBJECT.to_string());
|
||||
}
|
||||
SystemMessage::SecurejoinMessage => {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
if !step.is_empty() {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use mail_builder::headers::Header;
|
||||
use mailparse::{MailHeaderMap, addrparse_header};
|
||||
use pgp::armor;
|
||||
use pgp::packet::{Packet, PacketParser};
|
||||
use std::io::BufReader;
|
||||
use std::str;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -11,7 +14,7 @@ use crate::chat::{
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants;
|
||||
use crate::contact::Origin;
|
||||
use crate::contact::{Origin, import_vcard};
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::message;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
@@ -877,3 +880,85 @@ async fn test_no_empty_to_header() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses ASCII-armored message and checks that it only has PKESK and SEIPD packets.
|
||||
///
|
||||
/// Panics if SEIPD packets are not of expected version.
|
||||
fn assert_seipd_version(payload: &str, version: usize) {
|
||||
let cursor = Cursor::new(payload);
|
||||
let dearmor = armor::Dearmor::new(cursor);
|
||||
let packet_parser = PacketParser::new(BufReader::new(dearmor));
|
||||
for packet in packet_parser {
|
||||
match packet.unwrap() {
|
||||
Packet::PublicKeyEncryptedSessionKey(_pkesk) => {}
|
||||
Packet::SymEncryptedProtectedData(seipd) => {
|
||||
assert_eq!(seipd.version(), version);
|
||||
}
|
||||
packet => {
|
||||
panic!("Unexpected packet {:?}", packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that messages between two test accounts use SEIPDv2 and not SEIPDv1.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_use_seipdv2() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = alice.create_chat_id(bob).await;
|
||||
let sent = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
assert_seipd_version(&sent.payload, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that messages to keys that don't advertise SEIPDv2 support
|
||||
/// are sent using SEIPDv1.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_fallback_to_seipdv1() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
// vCard of Alice with no SEIPDv2 feature advertised in the key.
|
||||
let alice_vcard = "BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
EMAIL:alice@example.org
|
||||
FN:Alice
|
||||
KEY:data:application/pgp-keys;base64,mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
|
||||
REV:20250412T195751Z
|
||||
END:VCARD";
|
||||
let contact_ids = import_vcard(bob, alice_vcard).await.unwrap();
|
||||
let alice_contact_id = contact_ids[0];
|
||||
let chat_id = ChatId::create_for_contact(bob, alice_contact_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Bob sends a message to Alice with SEIPDv1 packet.
|
||||
let sent = bob.send_text(chat_id, "Hello!").await;
|
||||
assert_seipd_version(&sent.payload, 1);
|
||||
|
||||
// Bob creates a group with Alice and Charlie.
|
||||
// Sending a message there should also use SEIPDv1
|
||||
// because for Bob it looks like Alice does not support SEIPDv2.
|
||||
let charlie_contact_id = bob.add_or_lookup_contact_id(charlie).await;
|
||||
let group_id = create_group(bob, "groupname").await.unwrap();
|
||||
chat::add_contact_to_chat(bob, group_id, alice_contact_id).await?;
|
||||
chat::add_contact_to_chat(bob, group_id, charlie_contact_id).await?;
|
||||
|
||||
let sent = bob.send_text(group_id, "Hello!").await;
|
||||
assert_seipd_version(&sent.payload, 1);
|
||||
|
||||
// Bob gets a new key of Alice via new vCard
|
||||
// and learns that Alice supports SEIPDv2.
|
||||
assert_eq!(bob.add_or_lookup_contact_id(alice).await, alice_contact_id);
|
||||
|
||||
let sent = bob.send_text(group_id, "Hello again with SEIPDv2!").await;
|
||||
assert_seipd_version(&sent.payload, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -86,7 +86,9 @@ pub(crate) struct MimeMessage {
|
||||
/// messages to this address to post them to the list.
|
||||
pub list_post: Option<String>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
/// Decryption error if decryption of the message has failed.
|
||||
pub decryption_error: Option<String>,
|
||||
|
||||
/// Valid signature fingerprint if a message is an
|
||||
/// Autocrypt encrypted and signed message and corresponding intended recipient fingerprints
|
||||
@@ -199,6 +201,9 @@ pub enum SystemMessage {
|
||||
MemberRemovedFromGroup = 5,
|
||||
|
||||
/// Autocrypt Setup Message.
|
||||
///
|
||||
/// Deprecated as of 2026-03-15, such messages should not be created
|
||||
/// but may exist in the database.
|
||||
AutocryptSetupMessage = 6,
|
||||
|
||||
/// Secure-join message.
|
||||
@@ -259,8 +264,6 @@ pub enum SystemMessage {
|
||||
GroupDescriptionChanged = 70,
|
||||
}
|
||||
|
||||
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||
|
||||
impl MimeMessage {
|
||||
/// Parse a mime message.
|
||||
///
|
||||
@@ -370,7 +373,7 @@ impl MimeMessage {
|
||||
hop_info += "\n\n";
|
||||
hop_info += &dkim_results.to_string();
|
||||
|
||||
let incoming = !context.is_self_addr(&from.addr).await?;
|
||||
let from_is_not_self_addr = !context.is_self_addr(&from.addr).await?;
|
||||
|
||||
let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
|
||||
|
||||
@@ -435,7 +438,7 @@ impl MimeMessage {
|
||||
};
|
||||
|
||||
let mut autocrypt_header = None;
|
||||
if incoming {
|
||||
if from_is_not_self_addr {
|
||||
// See `get_all_addresses_from_header()` for why we take the last valid header.
|
||||
for val in aheader_values.iter().rev() {
|
||||
autocrypt_header = match Aheader::from_str(val) {
|
||||
@@ -466,7 +469,7 @@ impl MimeMessage {
|
||||
None
|
||||
};
|
||||
|
||||
let mut public_keyring = if incoming {
|
||||
let mut public_keyring = if from_is_not_self_addr {
|
||||
if let Some(autocrypt_header) = autocrypt_header {
|
||||
vec![autocrypt_header.public_key]
|
||||
} else {
|
||||
@@ -651,6 +654,15 @@ impl MimeMessage {
|
||||
.into_iter()
|
||||
.last()
|
||||
.map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::<HashSet<_>>()));
|
||||
|
||||
let incoming = if let Some((ref sig_fp, _)) = signature {
|
||||
sig_fp.hex() != key::self_fingerprint(context).await?
|
||||
} else {
|
||||
// rare case of getting a cleartext message
|
||||
// so we determine 'incoming' flag by From-address
|
||||
from_is_not_self_addr
|
||||
};
|
||||
|
||||
let mut parser = MimeMessage {
|
||||
parts: Vec::new(),
|
||||
headers,
|
||||
@@ -663,7 +675,7 @@ impl MimeMessage {
|
||||
from,
|
||||
incoming,
|
||||
chat_disposition_notification_to,
|
||||
decrypting_failed: mail.is_err(),
|
||||
decryption_error: mail.err().map(|err| format!("{err:#}")),
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signature,
|
||||
@@ -742,20 +754,8 @@ impl MimeMessage {
|
||||
}
|
||||
|
||||
/// Parses system messages.
|
||||
fn parse_system_message_headers(&mut self, context: &Context) {
|
||||
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
|
||||
self.parts.retain(|part| {
|
||||
part.mimetype
|
||||
.as_ref()
|
||||
.is_none_or(|mimetype| mimetype.as_ref() == MIME_AC_SETUP_FILE)
|
||||
});
|
||||
|
||||
if self.parts.len() == 1 {
|
||||
self.is_system_message = SystemMessage::AutocryptSetupMessage;
|
||||
} else {
|
||||
warn!(context, "could not determine ASM mime-part");
|
||||
}
|
||||
} else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
|
||||
fn parse_system_message_headers(&mut self) {
|
||||
if let Some(value) = self.get_header(HeaderDef::ChatContent) {
|
||||
if value == "location-streaming-enabled" {
|
||||
self.is_system_message = SystemMessage::LocationStreamingEnabled;
|
||||
} else if value == "ephemeral-timer-changed" {
|
||||
@@ -905,7 +905,7 @@ impl MimeMessage {
|
||||
}
|
||||
|
||||
async fn parse_headers(&mut self, context: &Context) -> Result<()> {
|
||||
self.parse_system_message_headers(context);
|
||||
self.parse_system_message_headers();
|
||||
self.parse_avatar_headers(context)?;
|
||||
self.parse_videochat_headers();
|
||||
if self.delivery_report.is_none() {
|
||||
@@ -916,7 +916,7 @@ impl MimeMessage {
|
||||
&& let Some(ref subject) = self.get_subject()
|
||||
{
|
||||
let mut prepend_subject = true;
|
||||
if !self.decrypting_failed {
|
||||
if self.decryption_error.is_none() {
|
||||
let colon = subject.find(':');
|
||||
if colon == Some(2)
|
||||
|| colon == Some(3)
|
||||
@@ -957,7 +957,7 @@ impl MimeMessage {
|
||||
self.parse_attachments();
|
||||
|
||||
// See if an MDN is requested from the other side
|
||||
if !self.decrypting_failed
|
||||
if self.decryption_error.is_none()
|
||||
&& !self.parts.is_empty()
|
||||
&& let Some(ref dn_to) = self.chat_disposition_notification_to
|
||||
{
|
||||
@@ -1089,7 +1089,7 @@ impl MimeMessage {
|
||||
#[cfg(test)]
|
||||
/// Returns whether the decrypted data contains the given `&str`.
|
||||
pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
|
||||
assert!(!self.decrypting_failed);
|
||||
assert!(self.decryption_error.is_none());
|
||||
let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
|
||||
decoded_str.contains(s)
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ async fn test_qr_code_security() -> Result<()> {
|
||||
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
|
||||
|
||||
let alice_fp = self_fingerprint(alice).await?;
|
||||
let secret_for_encryption = dbg!(format!("securejoin/{alice_fp}/{authcode}"));
|
||||
let secret_for_encryption = format!("securejoin/{alice_fp}/{authcode}");
|
||||
test_shared_secret_decryption_ex(
|
||||
bob,
|
||||
&charlie_addr,
|
||||
|
||||
@@ -881,7 +881,7 @@ fn merge_with_cache(
|
||||
) -> Vec<SocketAddr> {
|
||||
let rest = resolved_addrs.split_off(std::cmp::min(resolved_addrs.len(), 2));
|
||||
|
||||
for addr in cache.into_iter().chain(rest.into_iter()) {
|
||||
for addr in cache.into_iter().chain(rest) {
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
if resolved_addrs.len() >= 10 {
|
||||
|
||||
@@ -7,8 +7,12 @@ use anyhow::Result;
|
||||
|
||||
use crate::net::session::SessionStream;
|
||||
|
||||
use tokio_rustls::rustls;
|
||||
use tokio_rustls::rustls::client::ClientSessionStore;
|
||||
|
||||
mod danger;
|
||||
use danger::NoCertificateVerification;
|
||||
|
||||
pub async fn wrap_tls<'a>(
|
||||
strict_tls: bool,
|
||||
hostname: &str,
|
||||
@@ -82,7 +86,7 @@ impl TlsSessionStore {
|
||||
.lock()
|
||||
.entry((port, alpn.to_string()))
|
||||
.or_insert_with(|| {
|
||||
Arc::new(tokio_rustls::rustls::client::ClientSessionMemoryCache::new(
|
||||
Arc::new(rustls::client::ClientSessionMemoryCache::new(
|
||||
TLS_CACHE_SIZE,
|
||||
))
|
||||
}),
|
||||
@@ -98,10 +102,10 @@ pub async fn wrap_rustls<'a>(
|
||||
stream: impl SessionStream + 'a,
|
||||
tls_session_store: &TlsSessionStore,
|
||||
) -> Result<impl SessionStream + 'a> {
|
||||
let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||
let mut root_cert_store = rustls::RootCertStore::empty();
|
||||
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
|
||||
let mut config = tokio_rustls::rustls::ClientConfig::builder()
|
||||
let mut config = rustls::ClientConfig::builder()
|
||||
.with_root_certificates(root_cert_store)
|
||||
.with_no_client_auth();
|
||||
config.alpn_protocols = if alpn.is_empty() {
|
||||
@@ -118,13 +122,25 @@ pub async fn wrap_rustls<'a>(
|
||||
// and are not worth increasing
|
||||
// attack surface: <https://words.filippo.io/we-need-to-talk-about-session-tickets/>.
|
||||
let resumption_store = tls_session_store.get(port, alpn);
|
||||
let resumption = tokio_rustls::rustls::client::Resumption::store(resumption_store)
|
||||
.tls12_resumption(tokio_rustls::rustls::client::Tls12Resumption::Disabled);
|
||||
let resumption = rustls::client::Resumption::store(resumption_store)
|
||||
.tls12_resumption(rustls::client::Tls12Resumption::Disabled);
|
||||
config.resumption = resumption;
|
||||
config.enable_sni = use_sni;
|
||||
|
||||
// Do not verify certificates for hostnames starting with `_`.
|
||||
// They are used for servers with self-signed certificates, e.g. for local testing.
|
||||
// Hostnames starting with `_` can have only self-signed TLS certificates or wildcard certificates.
|
||||
// It is not possible to get valid non-wildcard TLS certificates because CA/Browser Forum requirements
|
||||
// explicitly state that domains should start with a letter, digit or hyphen:
|
||||
// https://github.com/cabforum/servercert/blob/24f38fd4765e019db8bb1a8c56bf63c7115ce0b0/docs/BR.md
|
||||
if hostname.starts_with("_") {
|
||||
config
|
||||
.dangerous()
|
||||
.set_certificate_verifier(Arc::new(NoCertificateVerification::new()));
|
||||
}
|
||||
|
||||
let tls = tokio_rustls::TlsConnector::from(Arc::new(config));
|
||||
let name = rustls_pki_types::ServerName::try_from(hostname)?.to_owned();
|
||||
let name = tokio_rustls::rustls::pki_types::ServerName::try_from(hostname)?.to_owned();
|
||||
let tls_stream = tls.connect(name, stream).await?;
|
||||
Ok(tls_stream)
|
||||
}
|
||||
|
||||
55
src/net/tls/danger.rs
Normal file
55
src/net/tls/danger.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! Dangerous TLS implementation of accepting invalid certificates for Rustls.
|
||||
|
||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use tokio_rustls::rustls;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct NoCertificateVerification();
|
||||
|
||||
impl NoCertificateVerification {
|
||||
pub(super) fn new() -> Self {
|
||||
Self()
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
let supported_schemes = &provider.signature_verification_algorithms;
|
||||
rustls::crypto::verify_tls12_signature(message, cert, dss, supported_schemes)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
let supported_schemes = &provider.signature_verification_algorithms;
|
||||
rustls::crypto::verify_tls13_signature(message, cert, dss, supported_schemes)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
provider
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
163
src/pgp.rs
163
src/pgp.rs
@@ -1,15 +1,14 @@
|
||||
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::{BufRead, Cursor};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use deltachat_contact_tools::{EmailAddress, may_be_valid_addr};
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
ArmorOptions, Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType,
|
||||
Message, MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey,
|
||||
SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey,
|
||||
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
};
|
||||
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
@@ -26,45 +25,9 @@ use tokio::runtime::Handle;
|
||||
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
|
||||
pub(crate) const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
|
||||
|
||||
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
|
||||
///
|
||||
/// Returns (type, headers, base64 encoded body).
|
||||
pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>)> {
|
||||
use std::io::Read;
|
||||
|
||||
let cursor = Cursor::new(buf);
|
||||
let mut dearmor = pgp::armor::Dearmor::new(cursor);
|
||||
|
||||
let mut bytes = Vec::with_capacity(buf.len());
|
||||
|
||||
dearmor.read_to_end(&mut bytes)?;
|
||||
let typ = dearmor.typ.context("failed to parse type")?;
|
||||
|
||||
// normalize headers
|
||||
let headers = dearmor
|
||||
.headers
|
||||
.into_iter()
|
||||
.map(|(key, values)| {
|
||||
(
|
||||
key.trim().to_lowercase(),
|
||||
values
|
||||
.last()
|
||||
.map_or_else(String::new, |s| s.trim().to_string()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((typ, headers, bytes))
|
||||
}
|
||||
|
||||
/// Create a new key pair.
|
||||
///
|
||||
/// Both secret and public key consist of signing primary key and encryption subkey
|
||||
@@ -151,7 +114,6 @@ pub async fn pk_encrypt(
|
||||
public_keys_for_encryption: Vec<SignedPublicKey>,
|
||||
private_key_for_signing: SignedSecretKey,
|
||||
compress: bool,
|
||||
anonymous_recipients: bool,
|
||||
seipd_version: SeipdVersion,
|
||||
) -> Result<String> {
|
||||
Handle::current()
|
||||
@@ -166,13 +128,7 @@ pub async fn pk_encrypt(
|
||||
hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime(
|
||||
pgp::types::Timestamp::now(),
|
||||
))?);
|
||||
// Test "elena" uses old Delta Chat.
|
||||
let skip = private_key_for_signing.dc_fingerprint().hex()
|
||||
== "B86586B6DEF437D674BFAFC02A6B2EBC633B9E82";
|
||||
for key in &public_keys_for_encryption {
|
||||
if skip {
|
||||
break;
|
||||
}
|
||||
let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint());
|
||||
let subpkt = match private_key_for_signing.version() < KeyVersion::V6 {
|
||||
true => Subpacket::regular(data)?,
|
||||
@@ -198,11 +154,7 @@ pub async fn pk_encrypt(
|
||||
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
|
||||
|
||||
for pkey in pkeys {
|
||||
if anonymous_recipients {
|
||||
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
|
||||
} else {
|
||||
msg.encrypt_to_key(&mut rng, &pkey)?;
|
||||
}
|
||||
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
|
||||
}
|
||||
|
||||
let hash_algorithm = private_key_for_signing.hash_alg();
|
||||
@@ -227,11 +179,7 @@ pub async fn pk_encrypt(
|
||||
);
|
||||
|
||||
for pkey in pkeys {
|
||||
if anonymous_recipients {
|
||||
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
|
||||
} else {
|
||||
msg.encrypt_to_key(&mut rng, &pkey)?;
|
||||
}
|
||||
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
|
||||
}
|
||||
|
||||
let hash_algorithm = private_key_for_signing.hash_alg();
|
||||
@@ -344,24 +292,6 @@ pub fn pk_validate(
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Symmetric encryption for the autocrypt setup message (ASM).
|
||||
pub async fn symm_encrypt_autocrypt_setup(passphrase: &str, plain: Vec<u8>) -> Result<String> {
|
||||
let passphrase = Password::from(passphrase.to_string());
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut rng = thread_rng();
|
||||
let s2k = StringToKey::new_default(&mut rng);
|
||||
let builder = MessageBuilder::from_bytes("", plain);
|
||||
let mut builder = builder.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
|
||||
builder.encrypt_with_password(s2k, &passphrase)?;
|
||||
|
||||
let encoded_msg = builder.to_armored_string(&mut rng, Default::default())?;
|
||||
|
||||
Ok(encoded_msg)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Symmetrically encrypt the message.
|
||||
/// This is used for broadcast channels and for version 2 of the Securejoin protocol.
|
||||
/// `shared secret` is the secret that will be used for symmetric encryption.
|
||||
@@ -405,23 +335,6 @@ pub async fn symm_encrypt_message(
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Symmetric decryption.
|
||||
pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
|
||||
passphrase: &str,
|
||||
ctext: T,
|
||||
) -> Result<Vec<u8>> {
|
||||
let passphrase = passphrase.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let (enc_msg, _) = Message::from_armor(ctext)?;
|
||||
let password = Password::from(passphrase);
|
||||
|
||||
let msg = enc_msg.decrypt_with_password(&password)?;
|
||||
let res = msg.decompress()?.as_data_vec()?;
|
||||
Ok(res)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Merges and minimizes OpenPGP certificates.
|
||||
///
|
||||
/// Keeps at most one direct key signature and
|
||||
@@ -582,6 +495,35 @@ pub(crate) fn addresses_from_public_key(public_key: &SignedPublicKey) -> Option<
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns true if public key advertises SEIPDv2 feature.
|
||||
pub(crate) fn pubkey_supports_seipdv2(public_key: &SignedPublicKey) -> bool {
|
||||
// If any Direct Key Signature or any User ID signature has SEIPDv2 feature,
|
||||
// assume that recipient can handle SEIPDv2.
|
||||
//
|
||||
// Third-party User ID signatures are dropped during certificate merging.
|
||||
// We don't check if the User ID is primary User ID.
|
||||
// Primary User ID is preferred during merging
|
||||
// and if some key has only non-primary User ID
|
||||
// it is acceptable. It is anyway unlikely that SEIPDv2
|
||||
// is advertised in a key without DKS or primary User ID.
|
||||
public_key
|
||||
.details
|
||||
.direct_signatures
|
||||
.iter()
|
||||
.chain(
|
||||
public_key
|
||||
.details
|
||||
.users
|
||||
.iter()
|
||||
.flat_map(|user| user.signatures.iter()),
|
||||
)
|
||||
.any(|signature| {
|
||||
signature
|
||||
.features()
|
||||
.is_some_and(|features| features.seipd_v2())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::LazyLock;
|
||||
@@ -596,7 +538,7 @@ mod tests {
|
||||
test_utils::{TestContext, TestContextManager, alice_keypair, bob_keypair},
|
||||
token,
|
||||
};
|
||||
use pgp::composed::Esk;
|
||||
use pgp::composed::{Esk, Message};
|
||||
use pgp::packet::PublicKeyEncryptedSessionKey;
|
||||
|
||||
async fn decrypt_bytes(
|
||||
@@ -641,33 +583,6 @@ mod tests {
|
||||
Ok((msg, ret_signature_fingerprints, content))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_1() {
|
||||
let (typ, _headers, base64) = split_armored_data(
|
||||
b"-----BEGIN PGP MESSAGE-----\nNoVal:\n\naGVsbG8gd29ybGQ=\n-----END PGP MESSAGE-----",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(typ, BlockType::Message);
|
||||
assert!(!base64.is_empty());
|
||||
assert_eq!(
|
||||
std::string::String::from_utf8(base64).unwrap(),
|
||||
"hello world"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_2() {
|
||||
let (typ, headers, base64) = split_armored_data(
|
||||
b"-----BEGIN PGP PRIVATE KEY BLOCK-----\nAutocrypt-Prefer-Encrypt: mutual \n\naGVsbG8gd29ybGQ=\n-----END PGP PRIVATE KEY BLOCK-----"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(typ, BlockType::PrivateKey);
|
||||
assert!(!base64.is_empty());
|
||||
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_keypair() {
|
||||
let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap();
|
||||
@@ -707,7 +622,6 @@ mod tests {
|
||||
|
||||
/// A ciphertext encrypted to Alice & Bob, signed by Alice.
|
||||
async fn ctext_signed() -> &'static String {
|
||||
let anonymous_recipients = true;
|
||||
CTEXT_SIGNED
|
||||
.get_or_init(|| async {
|
||||
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
|
||||
@@ -718,7 +632,6 @@ mod tests {
|
||||
keyring,
|
||||
KEYS.alice_secret.clone(),
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
SeipdVersion::V2,
|
||||
)
|
||||
.await
|
||||
@@ -905,12 +818,12 @@ mod tests {
|
||||
let pk_for_encryption = load_self_public_key(alice).await?;
|
||||
|
||||
// Encrypt a message, but only to self, not to Bob:
|
||||
let compress = true;
|
||||
let ctext = pk_encrypt(
|
||||
plain,
|
||||
vec![pk_for_encryption],
|
||||
KEYS.alice_secret.clone(),
|
||||
true,
|
||||
true,
|
||||
compress,
|
||||
SeipdVersion::V2,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -136,6 +136,7 @@ impl PushSubscriber {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(context, "Subscribing for heartbeat notifications.");
|
||||
if http::post_string(
|
||||
context,
|
||||
"https://notifications.delta.chat/register",
|
||||
@@ -143,6 +144,7 @@ impl PushSubscriber {
|
||||
)
|
||||
.await?
|
||||
{
|
||||
info!(context, "Subscribed for heartbeat notifications.");
|
||||
state.heartbeat_subscribed = true;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
12
src/qr.rs
12
src/qr.rs
@@ -682,6 +682,12 @@ fn decode_account(qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(DCACCOUNT_SCHEME.len()..)
|
||||
.context("Invalid DCACCOUNT payload")?;
|
||||
|
||||
// Handle `dcaccount://...` URLs.
|
||||
let payload = payload.strip_prefix("//").unwrap_or(payload);
|
||||
if payload.is_empty() {
|
||||
bail!("dcaccount payload is empty");
|
||||
}
|
||||
if payload.starts_with("https://") {
|
||||
let url = url::Url::parse(payload).context("Invalid account URL")?;
|
||||
if url.scheme() == "https" {
|
||||
@@ -695,6 +701,12 @@ fn decode_account(qr: &str) -> Result<Qr> {
|
||||
bail!("Bad scheme for account URL: {:?}.", url.scheme());
|
||||
}
|
||||
} else {
|
||||
if payload.starts_with("/") {
|
||||
// Handle `dcaccount:///` URL reported to have been created
|
||||
// by Telegram link parser at
|
||||
// <https://support.delta.chat/t/could-not-find-dns-resolutions-for-imap-993-when-adding-a-relay/4907>
|
||||
bail!("Hostname in dcaccount URL cannot start with /");
|
||||
}
|
||||
Ok(Qr::Account {
|
||||
domain: payload.to_string(),
|
||||
})
|
||||
|
||||
@@ -721,7 +721,9 @@ async fn test_decode_account() -> Result<()> {
|
||||
|
||||
for text in [
|
||||
"DCACCOUNT:example.org",
|
||||
"DCACCOUNT://example.org",
|
||||
"dcaccount:example.org",
|
||||
"dcaccount://example.org",
|
||||
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
] {
|
||||
@@ -737,6 +739,21 @@ async fn test_decode_account() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that decoding empty `dcaccount://` URL results in an error.
|
||||
/// We should not suggest trying to configure an account in this case.
|
||||
/// Such links may be created by copy-paste error or because of incorrect parsing.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_empty_account() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
for text in ["DCACCOUNT:", "dcaccount:", "dcaccount://", "dcaccount:///"] {
|
||||
let qr = check_qr(&ctx.ctx, text).await;
|
||||
assert!(qr.is_err(), "Invalid {text:?} is parsed as dcaccount URL");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_tg_socks_proxy() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -111,10 +111,10 @@ async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Resu
|
||||
|
||||
let qrcode_description = match chat.typ {
|
||||
crate::constants::Chattype::Group => {
|
||||
stock_str::secure_join_group_qr_description(context, &chat).await
|
||||
stock_str::secure_join_group_qr_description(context, &chat)
|
||||
}
|
||||
crate::constants::Chattype::OutBroadcast => {
|
||||
stock_str::secure_join_broadcast_qr_description(context, &chat).await
|
||||
stock_str::secure_join_broadcast_qr_description(context, &chat)
|
||||
}
|
||||
_ => bail!("Unexpected chat type {}", chat.typ),
|
||||
};
|
||||
@@ -132,7 +132,7 @@ async fn generate_verification_qr(context: &Context) -> Result<String> {
|
||||
let (avatar, displayname, addr, color) = self_info(context).await?;
|
||||
|
||||
inner_generate_secure_join_qr_code(
|
||||
&stock_str::setup_contact_qr_description(context, &displayname, &addr).await,
|
||||
&stock_str::setup_contact_qr_description(context, &displayname, &addr),
|
||||
&securejoin::get_securejoin_qr(context, None).await?,
|
||||
&color,
|
||||
avatar,
|
||||
|
||||
24
src/quota.rs
24
src/quota.rs
@@ -109,10 +109,9 @@ impl Context {
|
||||
/// called.
|
||||
pub(crate) async fn quota_needs_update(&self, transport_id: u32, ratelimit_secs: u64) -> bool {
|
||||
let quota = self.quota.read().await;
|
||||
quota
|
||||
.get(&transport_id)
|
||||
.filter(|quota| time_elapsed("a.modified) < Duration::from_secs(ratelimit_secs))
|
||||
.is_none()
|
||||
quota.get(&transport_id).is_none_or(|quota| {
|
||||
time_elapsed("a.modified) >= Duration::from_secs(ratelimit_secs)
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates `quota.recent`, sets `quota.modified` to the current time
|
||||
@@ -124,11 +123,15 @@ impl Context {
|
||||
/// in case for some providers the quota is always at ~100%
|
||||
/// and new space is allocated as needed.
|
||||
pub(crate) async fn update_recent_quota(&self, session: &mut ImapSession) -> Result<()> {
|
||||
let transport_id = session.transport_id();
|
||||
|
||||
info!(self, "Transport {transport_id}: Updating quota.");
|
||||
|
||||
let quota = if session.can_check_quota() {
|
||||
let folders = get_watched_folders(self).await?;
|
||||
get_unique_quota_roots_and_usage(session, folders).await
|
||||
} else {
|
||||
Err(anyhow!(stock_str::not_supported_by_provider(self).await))
|
||||
Err(anyhow!(stock_str::not_supported_by_provider(self)))
|
||||
};
|
||||
|
||||
if let Ok(quota) = "a {
|
||||
@@ -143,26 +146,29 @@ impl Context {
|
||||
Some(&highest.to_string()),
|
||||
)
|
||||
.await?;
|
||||
let mut msg =
|
||||
Message::new_text(stock_str::quota_exceeding(self, highest).await);
|
||||
let mut msg = Message::new_text(stock_str::quota_exceeding(self, highest));
|
||||
add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
|
||||
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
|
||||
self.set_config_internal(Config::QuotaExceeding, None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(err) => warn!(self, "cannot get highest quota usage: {:#}", err),
|
||||
Err(err) => warn!(
|
||||
self,
|
||||
"Transport {transport_id}: Cannot get highest quota usage: {err:#}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
self.quota.write().await.insert(
|
||||
session.transport_id(),
|
||||
transport_id,
|
||||
QuotaInfo {
|
||||
recent: quota,
|
||||
modified: tools::Time::now(),
|
||||
},
|
||||
);
|
||||
|
||||
info!(self, "Transport {transport_id}: Updated quota.");
|
||||
self.emit_event(EventType::ConnectivityChanged);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
155
src/reaction.rs
155
src/reaction.rs
@@ -347,8 +347,7 @@ impl Chat {
|
||||
if self
|
||||
.param
|
||||
.get_i64(Param::LastReactionTimestamp)
|
||||
.filter(|&reaction_timestamp| reaction_timestamp > timestamp)
|
||||
.is_none()
|
||||
.is_none_or(|reaction_timestamp| reaction_timestamp <= timestamp)
|
||||
{
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -393,7 +392,9 @@ mod tests {
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::message::{MessageState, Viewtype, delete_msgs};
|
||||
use crate::key::{load_self_public_key, load_self_secret_key};
|
||||
use crate::message::{MessageState, Viewtype, delete_msgs, markseen_msgs};
|
||||
use crate::pgp::{SeipdVersion, pk_encrypt};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::E2EE_INFO_MSGS;
|
||||
@@ -956,4 +957,152 @@ Content-Disposition: reaction\n\
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if reaction requests a read receipt,
|
||||
/// no read receipt is sent when the chat is marked as noticed.
|
||||
///
|
||||
/// Reactions create hidden messages in the chat,
|
||||
/// and when marking the chat as noticed marks
|
||||
/// such messages as seen, read receipts should never be sent
|
||||
/// to avoid the sender of reaction from learning
|
||||
/// that receiver opened the chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction_request_mdn() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = alice.create_chat_id(bob).await;
|
||||
let alice_sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
|
||||
let bob_msg = bob.recv_msg(&alice_sent_msg).await;
|
||||
bob_msg.chat_id.accept(bob).await?;
|
||||
assert_eq!(bob_msg.state, MessageState::InFresh);
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
markseen_msgs(bob, vec![bob_msg.id]).await?;
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id!=?",
|
||||
(ContactId::SELF,)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
bob.sql.execute("DELETE FROM smtp_mdns", ()).await?;
|
||||
|
||||
// Construct reaction with an MDN request.
|
||||
// Note the `Chat-Disposition-Notification-To` header.
|
||||
let known_id = bob_msg.rfc724_mid;
|
||||
let new_id = "e2b6e69e-4124-4e2a-b79f-e4f1be667165@localhost";
|
||||
|
||||
let plain_text = format!(
|
||||
"Content-Type: text/plain; charset=\"utf-8\"; protected-headers=\"v1\"; \r
|
||||
hp=\"cipher\"\r
|
||||
Content-Disposition: reaction\r
|
||||
From: \"Alice\" <alice@example.org>\r
|
||||
To: \"Bob\" <bob@example.net>\r
|
||||
Subject: Message from Alice\r
|
||||
Date: Sat, 14 Mar 2026 01:02:03 +0000\r
|
||||
In-Reply-To: <{known_id}>\r
|
||||
References: <{known_id}>\r
|
||||
Chat-Version: 1.0\r
|
||||
Chat-Disposition-Notification-To: alice@example.org\r
|
||||
Message-ID: <{new_id}>\r
|
||||
HP-Outer: From: <alice@example.org>\r
|
||||
HP-Outer: To: \"hidden-recipients\": ;\r
|
||||
HP-Outer: Subject: [...]\r
|
||||
HP-Outer: Date: Sat, 14 Mar 2026 01:02:03 +0000\r
|
||||
HP-Outer: Message-ID: <{new_id}>\r
|
||||
HP-Outer: In-Reply-To: <{known_id}>\r
|
||||
HP-Outer: References: <{known_id}>\r
|
||||
HP-Outer: Chat-Version: 1.0\r
|
||||
Content-Transfer-Encoding: base64\r
|
||||
\r
|
||||
8J+RgA==\r
|
||||
"
|
||||
);
|
||||
|
||||
let alice_public_key = load_self_public_key(alice).await?;
|
||||
let bob_public_key = load_self_public_key(bob).await?;
|
||||
let alice_secret_key = load_self_secret_key(alice).await?;
|
||||
let public_keys_for_encryption = vec![alice_public_key, bob_public_key];
|
||||
let compress = true;
|
||||
let encrypted_payload = pk_encrypt(
|
||||
plain_text.as_bytes().to_vec(),
|
||||
public_keys_for_encryption,
|
||||
alice_secret_key,
|
||||
compress,
|
||||
SeipdVersion::V2,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let boundary = "boundary123";
|
||||
let rcvd_mail = format!(
|
||||
"From: <alice@example.org>\r
|
||||
To: \"hidden-recipients\": ;\r
|
||||
Subject: [...]\r
|
||||
Date: Sat, 14 Mar 2026 01:02:03 +0000\r
|
||||
Message-ID: <{new_id}>\r
|
||||
In-Reply-To: <{known_id}>\r
|
||||
References: <{known_id}>\r
|
||||
Content-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\";\r
|
||||
boundary=\"{boundary}\"\r
|
||||
MIME-Version: 1.0\r
|
||||
\r
|
||||
--{boundary}\r
|
||||
Content-Type: application/pgp-encrypted; charset=\"utf-8\"\r
|
||||
Content-Description: PGP/MIME version identification\r
|
||||
Content-Transfer-Encoding: 7bit\r
|
||||
\r
|
||||
Version: 1\r
|
||||
\r
|
||||
--{boundary}\r
|
||||
Content-Type: application/octet-stream; name=\"encrypted.asc\";\r
|
||||
charset=\"utf-8\"\r
|
||||
Content-Description: OpenPGP encrypted message\r
|
||||
Content-Disposition: inline; filename=\"encrypted.asc\";\r
|
||||
Content-Transfer-Encoding: 7bit\r
|
||||
\r
|
||||
{encrypted_payload}
|
||||
--{boundary}--\r
|
||||
"
|
||||
);
|
||||
|
||||
let received = receive_imf(bob, rcvd_mail.as_bytes(), false)
|
||||
.await?
|
||||
.unwrap();
|
||||
let bob_hidden_msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(bob_hidden_msg.hidden);
|
||||
assert_eq!(bob_hidden_msg.chat_id, bob_chat_id);
|
||||
|
||||
// Bob does not see new message and cannot mark it as seen directly,
|
||||
// but can mark the chat as noticed when opening it.
|
||||
marknoticed_chat(bob, bob_chat_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id!=?",
|
||||
(ContactId::SELF,)
|
||||
)
|
||||
.await?,
|
||||
0,
|
||||
"Bob should not send MDN to Alice"
|
||||
);
|
||||
|
||||
// MDN request was ignored, but reaction was not.
|
||||
let reactions = get_msg_reactions(bob, bob_msg.id).await?;
|
||||
assert_eq!(reactions.reactions.len(), 1);
|
||||
assert_eq!(
|
||||
reactions.emoji_sorted_by_frequency(),
|
||||
vec![("👀".to_string(), 1)]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ use crate::securejoin::{
|
||||
self, get_secure_join_step, handle_securejoin_handshake, observe_securejoin_on_other_device,
|
||||
};
|
||||
use crate::simplify;
|
||||
use crate::smtp::msg_has_pending_smtp_job;
|
||||
use crate::stats::STATISTICS_BOT_EMAIL;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
@@ -582,14 +583,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
(rfc724_mid_orig, &self_addr),
|
||||
)
|
||||
.await?;
|
||||
if !context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
|
||||
(rfc724_mid_orig,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if !msg_has_pending_smtp_job(context, msg_id).await? {
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
return Ok(None);
|
||||
@@ -727,10 +721,9 @@ pub(crate) async fn receive_imf_inner(
|
||||
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default();
|
||||
|
||||
let allow_creation = if mime_parser.decrypting_failed {
|
||||
let allow_creation = if mime_parser.decryption_error.is_some() {
|
||||
false
|
||||
} else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
|
||||
&& is_dc_message == MessengerMessage::No
|
||||
} else if is_dc_message == MessengerMessage::No
|
||||
&& !context.get_config_bool(Config::IsChatmail).await?
|
||||
{
|
||||
// the message is a classic email in a classic profile
|
||||
@@ -880,8 +873,7 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
let instance = if mime_parser
|
||||
.parts
|
||||
.first()
|
||||
.filter(|part| part.typ == Viewtype::Webxdc)
|
||||
.is_some()
|
||||
.is_some_and(|part| part.typ == Viewtype::Webxdc)
|
||||
{
|
||||
can_info_msg = false;
|
||||
Some(
|
||||
@@ -1012,11 +1004,11 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
&& msg.chat_visibility == ChatVisibility::Archived;
|
||||
updated_chats
|
||||
.entry(msg.chat_id)
|
||||
.and_modify(|ts| *ts = cmp::max(*ts, msg.timestamp_sort))
|
||||
.or_insert(msg.timestamp_sort);
|
||||
.and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
|
||||
.or_insert((msg.timestamp_sort, msg.id));
|
||||
}
|
||||
}
|
||||
for (chat_id, timestamp_sort) in updated_chats {
|
||||
for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -1025,12 +1017,13 @@ UPDATE msgs SET state=? WHERE
|
||||
state=? AND
|
||||
hidden=0 AND
|
||||
chat_id=? AND
|
||||
timestamp<?",
|
||||
(timestamp,id)<(?,?)",
|
||||
(
|
||||
MessageState::InNoticed,
|
||||
MessageState::InFresh,
|
||||
chat_id,
|
||||
timestamp_sort,
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
@@ -1213,14 +1206,18 @@ async fn decide_chat_assignment(
|
||||
{
|
||||
info!(context, "Call state changed (TRASH).");
|
||||
true
|
||||
} else if mime_parser.decrypting_failed && !mime_parser.incoming {
|
||||
} else if let Some(ref decryption_error) = mime_parser.decryption_error
|
||||
&& !mime_parser.incoming
|
||||
{
|
||||
// Outgoing undecryptable message.
|
||||
let last_time = context
|
||||
.get_config_i64(Config::LastCantDecryptOutgoingMsgs)
|
||||
.await?;
|
||||
let now = tools::time();
|
||||
let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
|
||||
let txt = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions.";
|
||||
let txt = format!(
|
||||
"⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions. (Error: {decryption_error}, {rfc724_mid})."
|
||||
);
|
||||
let mut msg = Message::new_text(txt.to_string());
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
@@ -1322,15 +1319,35 @@ async fn decide_chat_assignment(
|
||||
// no database row and ChatId yet.
|
||||
let mut num_recipients = 0;
|
||||
let mut has_self_addr = false;
|
||||
for recipient in &mime_parser.recipients {
|
||||
has_self_addr |= context.is_self_addr(&recipient.addr).await?;
|
||||
if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
|
||||
continue;
|
||||
|
||||
if let Some((sender_fingerprint, intended_recipient_fingerprints)) = mime_parser
|
||||
.signature
|
||||
.as_ref()
|
||||
.filter(|(_sender_fingerprint, fps)| !fps.is_empty())
|
||||
{
|
||||
// The message is signed and has intended recipient fingerprints.
|
||||
|
||||
// If the message has intended recipient fingerprint and is not trashed already,
|
||||
// then it is intended for us.
|
||||
has_self_addr = true;
|
||||
|
||||
num_recipients = intended_recipient_fingerprints
|
||||
.iter()
|
||||
.filter(|fp| *fp != sender_fingerprint)
|
||||
.count();
|
||||
} else {
|
||||
// Message has no intended recipient fingerprints
|
||||
// or is not signed, count the `To` field recipients.
|
||||
for recipient in &mime_parser.recipients {
|
||||
has_self_addr |= context.is_self_addr(&recipient.addr).await?;
|
||||
if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
|
||||
continue;
|
||||
}
|
||||
num_recipients += 1;
|
||||
}
|
||||
if from_id != ContactId::SELF && !has_self_addr {
|
||||
num_recipients += 1;
|
||||
}
|
||||
num_recipients += 1;
|
||||
}
|
||||
if from_id != ContactId::SELF && !has_self_addr {
|
||||
num_recipients += 1;
|
||||
}
|
||||
let mut can_be_11_chat_log = String::new();
|
||||
let mut l = |cond: bool, s: String| {
|
||||
@@ -1817,6 +1834,16 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
// Sort message to the bottom if we are not in the chat
|
||||
// so if we are added via QR code scan
|
||||
// the message about our addition goes after all the info messages.
|
||||
// Info messages are sorted by local smeared_timestamp()
|
||||
// which advances quickly during SecureJoin,
|
||||
// while "member added" message may have older timestamp
|
||||
// corresponding to the sender clock.
|
||||
// In practice inviter clock may even be slightly in the past.
|
||||
let sort_to_bottom = !chat.is_self_in_chat(context).await?;
|
||||
|
||||
let is_location_kml = mime_parser.location_kml.is_some();
|
||||
let mut group_changes = match chat.typ {
|
||||
_ if chat.id.is_special() => GroupChangesInfo::default(),
|
||||
@@ -1867,16 +1894,8 @@ async fn add_parts(
|
||||
};
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
|
||||
let sort_to_bottom = false;
|
||||
let received = true;
|
||||
let sort_timestamp = chat_id
|
||||
.calc_sort_timestamp(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
sort_to_bottom,
|
||||
received,
|
||||
mime_parser.incoming,
|
||||
)
|
||||
.calc_sort_timestamp(context, mime_parser.timestamp_sent, sort_to_bottom)
|
||||
.await?;
|
||||
|
||||
// Apply ephemeral timer changes to the chat.
|
||||
@@ -2272,7 +2291,7 @@ RETURNING id
|
||||
if trash { 0 } else { ephemeral_timestamp },
|
||||
if trash {
|
||||
DownloadState::Done
|
||||
} else if mime_parser.decrypting_failed {
|
||||
} else if mime_parser.decryption_error.is_some() {
|
||||
DownloadState::Undecipherable
|
||||
} else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
|
||||
DownloadState::Available
|
||||
@@ -2336,7 +2355,7 @@ RETURNING id
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Message has {icnt} parts and is assigned to chat #{chat_id}."
|
||||
"Message has {icnt} parts and is assigned to chat #{chat_id}, timestamp={sort_timestamp}."
|
||||
);
|
||||
|
||||
if !chat_id.is_trash() && !hidden {
|
||||
@@ -2685,7 +2704,7 @@ async fn lookup_or_create_adhoc_group(
|
||||
allow_creation: bool,
|
||||
create_blocked: Blocked,
|
||||
) -> Result<Option<(ChatId, Blocked, bool)>> {
|
||||
if mime_parser.decrypting_failed {
|
||||
if mime_parser.decryption_error.is_some() {
|
||||
warn!(
|
||||
context,
|
||||
"Not creating ad-hoc group for message that cannot be decrypted."
|
||||
@@ -2907,7 +2926,7 @@ async fn create_group(
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
Ok(Some((chat_id, chat_id_blocked)))
|
||||
} else if mime_parser.decrypting_failed {
|
||||
} else if mime_parser.decryption_error.is_some() {
|
||||
// It is possible that the message was sent to a valid,
|
||||
// yet unknown group, which was rejected because
|
||||
// Chat-Group-Name, which is in the encrypted part, was
|
||||
@@ -3338,7 +3357,7 @@ async fn apply_chat_name_avatar_and_description_changes(
|
||||
let old_name = &sanitize_single_line(old_name);
|
||||
better_msg.get_or_insert(
|
||||
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
|
||||
stock_str::msg_broadcast_name_changed(context, old_name, grpname).await
|
||||
stock_str::msg_broadcast_name_changed(context, old_name, grpname)
|
||||
} else {
|
||||
stock_str::msg_grp_name(context, old_name, grpname, from_id).await
|
||||
},
|
||||
@@ -3401,7 +3420,7 @@ async fn apply_chat_name_avatar_and_description_changes(
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
better_msg.get_or_insert(
|
||||
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
|
||||
stock_str::msg_broadcast_img_changed(context).await
|
||||
stock_str::msg_broadcast_img_changed(context)
|
||||
} else {
|
||||
match avatar_action {
|
||||
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
|
||||
@@ -3841,7 +3860,7 @@ async fn apply_in_broadcast_changes(
|
||||
info!(context, "No-op broadcast 'Member added' message (TRASH)");
|
||||
"".to_string()
|
||||
} else {
|
||||
stock_str::msg_you_joined_broadcast(context).await
|
||||
stock_str::msg_you_joined_broadcast(context)
|
||||
};
|
||||
|
||||
better_msg.get_or_insert(msg);
|
||||
@@ -3857,7 +3876,7 @@ async fn apply_in_broadcast_changes(
|
||||
chat::delete_broadcast_secret(context, chat.id).await?;
|
||||
|
||||
if from_id == ContactId::SELF {
|
||||
better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context).await);
|
||||
better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context));
|
||||
} else {
|
||||
better_msg.get_or_insert(
|
||||
stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
|
||||
|
||||
@@ -13,9 +13,10 @@ use crate::constants::DC_GCL_FOR_FORWARDING;
|
||||
use crate::contact;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{ImexMode, imex};
|
||||
use crate::key;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::tools::{SystemTime, time};
|
||||
|
||||
@@ -819,9 +820,12 @@ async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
receive_imf(context, imf_raw, false).await.unwrap();
|
||||
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
let msg_id = chats.get_msg_id(0).unwrap().unwrap();
|
||||
let received_msg = receive_imf(context, imf_raw, false)
|
||||
.await
|
||||
.expect("receive_imf failure")
|
||||
.expect("No message received");
|
||||
assert_eq!(received_msg.msg_ids.len(), 1);
|
||||
let msg_id = received_msg.msg_ids[0];
|
||||
Message::load_from_db(context, msg_id).await.unwrap()
|
||||
}
|
||||
|
||||
@@ -2872,9 +2876,8 @@ async fn test_rfc1847_encapsulation() -> Result<()> {
|
||||
|
||||
// Alice sends a message to Bob using Thunderbird.
|
||||
let raw = include_bytes!("../../test-data/message/rfc1847_encapsulation.eml");
|
||||
receive_imf(bob, raw, false).await?;
|
||||
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = load_imf_email(bob, raw).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
@@ -3082,8 +3085,8 @@ async fn test_auto_accept_for_bots() -> Result<()> {
|
||||
async fn test_auto_accept_group_for_bots() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
receive_imf(&t, GRP_MAIL, false).await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
let msg = load_imf_email(&t, GRP_MAIL).await;
|
||||
|
||||
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert!(!chat.is_contact_request());
|
||||
Ok(())
|
||||
@@ -3327,7 +3330,7 @@ async fn test_outgoing_undecryptable() -> Result<()> {
|
||||
assert!(
|
||||
dev_msg
|
||||
.text
|
||||
.contains("⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions.")
|
||||
.starts_with("⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions. (Error:")
|
||||
);
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
|
||||
@@ -3556,9 +3559,9 @@ async fn test_messed_up_message_id() -> Result<()> {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/messed_up_message_id.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
let msg = load_imf_email(&t, raw).await;
|
||||
assert_eq!(
|
||||
t.get_last_msg().await.rfc724_mid,
|
||||
msg.rfc724_mid,
|
||||
"0bb9ffe1-2596-d997-95b4-1fef8cc4808e@example.org"
|
||||
);
|
||||
|
||||
@@ -5108,97 +5111,75 @@ async fn test_dont_verify_by_verified_by_unknown() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that second device assigns outgoing encrypted messages
|
||||
/// to 1:1 chat with key-contact even if the key of the contact is unknown.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recv_outgoing_msg_before_securejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
let a0 = &tcm.elena().await;
|
||||
let a1 = &tcm.elena().await;
|
||||
|
||||
tcm.execute_securejoin(bob, a0).await;
|
||||
let chat_id_a0_bob = a0.create_chat_id(bob).await;
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(chat_a1.typ, Chattype::Group);
|
||||
assert!(!chat_a1.is_encrypted(a1).await?);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(a1, chat_a1.id).await?,
|
||||
[a1.add_or_lookup_address_contact_id(bob).await]
|
||||
);
|
||||
assert_eq!(
|
||||
chat_a1.why_cant_send(a1).await?,
|
||||
Some(CantSendReason::NotAMember)
|
||||
);
|
||||
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi again").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
assert_eq!(msg_a1.chat_id, chat_a1.id);
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(
|
||||
chat_a1.why_cant_send(a1).await?,
|
||||
Some(CantSendReason::NotAMember)
|
||||
);
|
||||
|
||||
let msg_a1 = tcm.send_recv(bob, a1, "Hi back").await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(chat_a1.typ, Chattype::Single);
|
||||
assert!(chat_a1.is_encrypted(a1).await?);
|
||||
// Weird, but fine, anyway the bigger problem is the conversation split into two chats.
|
||||
assert_eq!(
|
||||
chat_a1.why_cant_send(a1).await?,
|
||||
Some(CantSendReason::ContactRequest)
|
||||
);
|
||||
|
||||
let a0 = &tcm.alice().await;
|
||||
let a1 = &tcm.alice().await;
|
||||
|
||||
tcm.execute_securejoin(bob, a0).await;
|
||||
let chat_id_a0_bob = a0.create_chat_id(bob).await;
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
// Device a1 does not have Bob's key.
|
||||
// Message is still received in an encrypted 1:1 chat with Bob.
|
||||
// a1 learns the fingerprint of Bob from the Intended Recipient Fingerprint packet,
|
||||
// but not the key.
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(chat_a1.typ, Chattype::Single);
|
||||
assert!(chat_a1.is_encrypted(a1).await?);
|
||||
|
||||
// Cannot send because a1 does not have Bob's key.
|
||||
assert!(!chat_a1.can_send(a1).await?);
|
||||
assert_eq!(
|
||||
chat_a1.why_cant_send(a1).await?,
|
||||
Some(CantSendReason::MissingKey)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(a1, chat_a1.id).await?,
|
||||
[a1.add_or_lookup_contact_id(bob).await]
|
||||
[a1.add_or_lookup_contact_id_no_key(bob).await]
|
||||
);
|
||||
assert!(chat_a1.can_send(a1).await?);
|
||||
assert!(!chat_a1.can_send(a1).await?);
|
||||
|
||||
let a1_chat_id = a1.create_chat_id(bob).await;
|
||||
assert_eq!(a1_chat_id, msg_a1.chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that outgoing message cannot be assigned to 1:1 chat
|
||||
/// without the intended recipient fingerprint.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recv_outgoing_msg_before_having_key_and_after() -> Result<()> {
|
||||
async fn test_recv_outgoing_msg_no_intended_recipient_fingerprint() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let a0 = &tcm.elena().await;
|
||||
let a1 = &tcm.elena().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
tcm.execute_securejoin(bob, a0).await;
|
||||
let chat_id_a0_bob = a0.create_chat_id(bob).await;
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi").await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(chat_a1.typ, Chattype::Group);
|
||||
assert!(!chat_a1.is_encrypted(a1).await?);
|
||||
let payload = include_bytes!(
|
||||
"../../test-data/message/alice_to_bob_no_intended_recipient_fingerprint.eml"
|
||||
);
|
||||
|
||||
// Alice does not have Bob's key.
|
||||
// Message is encrypted, but is received in ad hoc group with Bob's address.
|
||||
let rcvd_msg = receive_imf(alice, payload, false).await?.unwrap();
|
||||
let msg_alice = Message::load_from_db(alice, rcvd_msg.msg_ids[0]).await?;
|
||||
|
||||
assert!(msg_alice.get_showpadlock());
|
||||
let chat_alice = Chat::load_from_db(alice, msg_alice.chat_id).await?;
|
||||
assert_eq!(chat_alice.typ, Chattype::Group);
|
||||
assert!(!chat_alice.is_encrypted(alice).await?);
|
||||
|
||||
// Cannot send because Bob's key is unknown.
|
||||
assert!(!chat_alice.can_send(alice).await?);
|
||||
assert_eq!(
|
||||
chat_alice.why_cant_send(alice).await?,
|
||||
Some(CantSendReason::NotAMember)
|
||||
);
|
||||
|
||||
// Device a1 somehow learns Bob's key and creates the corresponding chat. However, this doesn't
|
||||
// help because we only look up key contacts by address in a particular chat and the new chat
|
||||
// isn't referenced by the received message. This is fixed by sending and receiving Intended
|
||||
// Recipient Fingerprint subpackets which elena doesn't send.
|
||||
a1.create_chat_id(bob).await;
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi again").await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
assert_eq!(msg_a1.chat_id, chat_a1.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5581,3 +5562,97 @@ async fn test_calendar_alternative() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that outgoing encrypted messages are detected
|
||||
/// by verifying own signature, completely ignoring From address.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing_determined_by_signature() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// alice_dev2: same key, different address.
|
||||
let different_from = "very@different.from";
|
||||
assert!(!alice.is_self_addr(different_from).await?);
|
||||
let alice_dev2 = &tcm.unconfigured().await;
|
||||
alice_dev2.configure_addr(different_from).await;
|
||||
key::store_self_keypair(alice_dev2, &alice_keypair()).await?;
|
||||
assert_ne!(
|
||||
alice.get_config(Config::Addr).await?.unwrap(),
|
||||
different_from
|
||||
);
|
||||
|
||||
// Send message from alice_dev2 and check alice sees it as outgoing
|
||||
let chat_id = alice_dev2.create_chat_id(bob).await;
|
||||
let sent_msg = alice_dev2.send_text(chat_id, "hello from new device").await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mark_message_as_delivered_only_after_sent_out_fully() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
alice.set_config_bool(Config::BccSelf, true).await?;
|
||||
let alice_chat_id = alice.create_chat_id(bob).await;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(alice, "a.jpg", file_bytes, None)?;
|
||||
let msg_id = chat::send_msg(alice, alice_chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (pre_msg_id, pre_msg_payload) = first_row_in_smtp_queue(alice).await;
|
||||
assert_eq!(msg_id, pre_msg_id);
|
||||
assert!(pre_msg_payload.len() < file_bytes.len());
|
||||
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
// Alice receives her own pre-message because of bcc_self
|
||||
// This should not yet mark the message as delivered,
|
||||
// because not everything was sent,
|
||||
// but it does remove the pre-message from the SMTP queue
|
||||
receive_imf(alice, pre_msg_payload.as_bytes(), false).await?;
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
|
||||
let (post_msg_id, post_msg_payload) = first_row_in_smtp_queue(alice).await;
|
||||
assert_eq!(msg_id, post_msg_id);
|
||||
assert!(post_msg_payload.len() > file_bytes.len());
|
||||
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
// Alice receives her own post-message because of bcc_self
|
||||
// This should now mark the message as delivered,
|
||||
// because everything was sent by now.
|
||||
receive_imf(alice, post_msg_payload.as_bytes(), false).await?;
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutDelivered);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the first sent message in the SMTP queue
|
||||
/// without removing it from the SMTP queue.
|
||||
/// This simulates the case that a message is successfully sent out,
|
||||
/// but the 'OK' answer from the server doesn't arrive,
|
||||
/// so that the SMTP row stays in the database.
|
||||
pub(crate) async fn first_row_in_smtp_queue(alice: &TestContext) -> (MsgId, String) {
|
||||
alice
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT id, msg_id, mime, recipients
|
||||
FROM smtp
|
||||
ORDER BY id"#,
|
||||
(),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(1)?;
|
||||
let mime: String = row.get(2)?;
|
||||
Ok((msg_id, mime))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("query_row_optional failed")
|
||||
.expect("No SMTP row found")
|
||||
}
|
||||
|
||||
@@ -355,6 +355,7 @@ async fn inbox_loop(
|
||||
stop_token,
|
||||
} = inbox_handlers;
|
||||
|
||||
let transport_id = connection.transport_id();
|
||||
let ctx1 = ctx.clone();
|
||||
let fut = async move {
|
||||
let ctx = ctx1;
|
||||
@@ -368,23 +369,34 @@ async fn inbox_loop(
|
||||
let session = if let Some(session) = old_session.take() {
|
||||
session
|
||||
} else {
|
||||
info!(ctx, "Preparing new IMAP session for inbox.");
|
||||
info!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Preparing new IMAP session for inbox."
|
||||
);
|
||||
match connection.prepare(&ctx).await {
|
||||
Err(err) => {
|
||||
warn!(ctx, "Failed to prepare inbox connection: {err:#}.");
|
||||
warn!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Failed to prepare inbox connection: {err:#}."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(session) => session,
|
||||
Ok(session) => {
|
||||
info!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Prepared new IMAP session for inbox."
|
||||
);
|
||||
session
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match inbox_fetch_idle(&ctx, &mut connection, session).await {
|
||||
Err(err) => warn!(ctx, "Failed inbox fetch_idle: {err:#}."),
|
||||
Err(err) => warn!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Failed inbox fetch_idle: {err:#}."
|
||||
),
|
||||
Ok(session) => {
|
||||
info!(
|
||||
ctx,
|
||||
"IMAP loop iteration for inbox finished, keeping the session."
|
||||
);
|
||||
old_session = Some(session);
|
||||
}
|
||||
}
|
||||
@@ -394,7 +406,7 @@ async fn inbox_loop(
|
||||
stop_token
|
||||
.cancelled()
|
||||
.map(|_| {
|
||||
info!(ctx, "Shutting down inbox loop.");
|
||||
info!(ctx, "Transport {transport_id}: Shutting down inbox loop.");
|
||||
})
|
||||
.race(fut)
|
||||
.await;
|
||||
@@ -431,17 +443,25 @@ pub async fn convert_folder_meaning(
|
||||
}
|
||||
|
||||
async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
|
||||
let transport_id = session.transport_id();
|
||||
|
||||
// Update quota no more than once a minute.
|
||||
if ctx.quota_needs_update(session.transport_id(), 60).await
|
||||
&& let Err(err) = ctx.update_recent_quota(&mut session).await
|
||||
{
|
||||
warn!(ctx, "Failed to update quota: {:#}.", err);
|
||||
warn!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Failed to update quota: {err:#}."
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(()) = imap.resync_request_receiver.try_recv()
|
||||
&& let Err(err) = session.resync_folders(ctx).await
|
||||
{
|
||||
warn!(ctx, "Failed to resync folders: {:#}.", err);
|
||||
warn!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Failed to resync folders: {err:#}."
|
||||
);
|
||||
imap.resync_request_sender.try_send(()).ok();
|
||||
}
|
||||
|
||||
@@ -456,7 +476,10 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(ctx, "Failed to get last housekeeping time: {}", err);
|
||||
warn!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Failed to get last housekeeping time: {err:#}"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -486,6 +509,8 @@ async fn fetch_idle(
|
||||
mut session: Session,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<Session> {
|
||||
let transport_id = session.transport_id();
|
||||
|
||||
let Some((folder_config, watch_folder)) = convert_folder_meaning(ctx, folder_meaning).await?
|
||||
else {
|
||||
// The folder is not configured.
|
||||
@@ -537,7 +562,7 @@ async fn fetch_idle(
|
||||
if !session.can_idle() {
|
||||
info!(
|
||||
ctx,
|
||||
"IMAP session does not support IDLE, going to fake idle."
|
||||
"Transport {transport_id}: IMAP session does not support IDLE, going to fake idle."
|
||||
);
|
||||
connection.fake_idle(ctx, watch_folder).await?;
|
||||
return Ok(session);
|
||||
@@ -550,15 +575,14 @@ async fn fetch_idle(
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
|
||||
info!(
|
||||
ctx,
|
||||
"Transport {transport_id}: IMAP IDLE is disabled, going to fake idle."
|
||||
);
|
||||
connection.fake_idle(ctx, watch_folder).await?;
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
info!(
|
||||
ctx,
|
||||
"IMAP session in folder {watch_folder:?} supports IDLE, using it."
|
||||
);
|
||||
let session = session
|
||||
.idle(
|
||||
ctx,
|
||||
@@ -619,10 +643,6 @@ async fn simple_imap_loop(
|
||||
match fetch_idle(&ctx, &mut connection, session, folder_meaning).await {
|
||||
Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"),
|
||||
Ok(session) => {
|
||||
info!(
|
||||
ctx,
|
||||
"IMAP loop iteration for {folder_meaning} finished, keeping the session"
|
||||
);
|
||||
old_session = Some(session);
|
||||
}
|
||||
}
|
||||
@@ -876,7 +896,7 @@ impl Scheduler {
|
||||
let timeout_duration = std::time::Duration::from_secs(30);
|
||||
|
||||
let tracker = TaskTracker::new();
|
||||
for b in self.inboxes.into_iter().chain(self.oboxes.into_iter()) {
|
||||
for b in self.inboxes.into_iter().chain(self.oboxes) {
|
||||
let context = context.clone();
|
||||
tracker.spawn(async move {
|
||||
tokio::time::timeout(timeout_duration, b.handle)
|
||||
|
||||
@@ -109,36 +109,36 @@ impl DetailedConnectivity {
|
||||
}
|
||||
}
|
||||
|
||||
async fn to_string_imap(&self, context: &Context) -> String {
|
||||
fn to_string_imap(&self, context: &Context) -> String {
|
||||
match self {
|
||||
DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
|
||||
DetailedConnectivity::Error(e) => stock_str::error(context, e),
|
||||
DetailedConnectivity::Uninitialized => "Not started".to_string(),
|
||||
DetailedConnectivity::Connecting => stock_str::connecting(context).await,
|
||||
DetailedConnectivity::Connecting => stock_str::connecting(context),
|
||||
DetailedConnectivity::Preparing | DetailedConnectivity::Working => {
|
||||
stock_str::updating(context).await
|
||||
stock_str::updating(context)
|
||||
}
|
||||
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
|
||||
stock_str::connected(context).await
|
||||
stock_str::connected(context)
|
||||
}
|
||||
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn to_string_smtp(&self, context: &Context) -> String {
|
||||
fn to_string_smtp(&self, context: &Context) -> String {
|
||||
match self {
|
||||
DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
|
||||
DetailedConnectivity::Error(e) => stock_str::error(context, e),
|
||||
DetailedConnectivity::Uninitialized => {
|
||||
"You did not try to send a message recently.".to_string()
|
||||
}
|
||||
DetailedConnectivity::Connecting => stock_str::connecting(context).await,
|
||||
DetailedConnectivity::Working => stock_str::sending(context).await,
|
||||
DetailedConnectivity::Connecting => stock_str::connecting(context),
|
||||
DetailedConnectivity::Working => stock_str::sending(context),
|
||||
|
||||
// We don't know any more than that the last message was sent successfully;
|
||||
// since sending the last message, connectivity could have changed, which we don't notice
|
||||
// until another message is sent
|
||||
DetailedConnectivity::InterruptingIdle
|
||||
| DetailedConnectivity::Preparing
|
||||
| DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context).await,
|
||||
| DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context),
|
||||
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -369,8 +369,8 @@ impl Context {
|
||||
.get_config_bool(crate::config::Config::ProxyEnabled)
|
||||
.await?
|
||||
{
|
||||
let proxy_enabled = stock_str::proxy_enabled(self).await;
|
||||
let proxy_description = stock_str::proxy_description(self).await;
|
||||
let proxy_enabled = stock_str::proxy_enabled(self);
|
||||
let proxy_description = stock_str::proxy_description(self);
|
||||
ret += &format!("<h3>{proxy_enabled}</h3><ul><li>{proxy_description}</li></ul>");
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ impl Context {
|
||||
_ => {
|
||||
ret += &format!(
|
||||
"<h3>{}</h3>\n</body></html>\n",
|
||||
stock_str::not_connected(self).await
|
||||
stock_str::not_connected(self)
|
||||
);
|
||||
return Ok(ret);
|
||||
}
|
||||
@@ -412,7 +412,7 @@ impl Context {
|
||||
// =============================================================================================
|
||||
|
||||
let watched_folders = get_watched_folder_configs(self).await?;
|
||||
let incoming_messages = stock_str::incoming_messages(self).await;
|
||||
let incoming_messages = stock_str::incoming_messages(self);
|
||||
ret += &format!("<h3>{incoming_messages}</h3><ul>");
|
||||
|
||||
let transports = self
|
||||
@@ -449,7 +449,7 @@ impl Context {
|
||||
ret += &*escaper::encode_minimal(&foldername);
|
||||
}
|
||||
ret += ":</b> ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self));
|
||||
ret += "<br />";
|
||||
|
||||
folder_added = true;
|
||||
@@ -464,7 +464,7 @@ impl Context {
|
||||
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self));
|
||||
ret += "<br />";
|
||||
}
|
||||
}
|
||||
@@ -504,13 +504,12 @@ impl Context {
|
||||
);
|
||||
}
|
||||
|
||||
let messages = stock_str::messages(self).await;
|
||||
let messages = stock_str::messages(self);
|
||||
let part_of_total_used = stock_str::part_of_total_used(
|
||||
self,
|
||||
&resource.usage.to_string(),
|
||||
&resource.limit.to_string(),
|
||||
)
|
||||
.await;
|
||||
);
|
||||
ret += &match &resource.name {
|
||||
Atom(resource_name) => {
|
||||
format!(
|
||||
@@ -531,7 +530,7 @@ impl Context {
|
||||
// - most times, this is the only item anyway
|
||||
let usage = &format_size(resource.usage * 1024, BINARY);
|
||||
let limit = &format_size(resource.limit * 1024, BINARY);
|
||||
stock_str::part_of_total_used(self, usage, limit).await
|
||||
stock_str::part_of_total_used(self, usage, limit)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -565,12 +564,12 @@ impl Context {
|
||||
// Your last message was sent successfully
|
||||
// =============================================================================================
|
||||
|
||||
let outgoing_messages = stock_str::outgoing_messages(self).await;
|
||||
let outgoing_messages = stock_str::outgoing_messages(self);
|
||||
ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
|
||||
let detailed = smtp.get_detailed();
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self));
|
||||
ret += "</li></ul>";
|
||||
|
||||
// =============================================================================================
|
||||
|
||||
@@ -450,10 +450,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
) {
|
||||
let mut self_found = false;
|
||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||
for (addr, key) in &mime_message.gossiped_keys {
|
||||
if key.public_key.dc_fingerprint() == self_fingerprint
|
||||
&& context.is_self_addr(addr).await?
|
||||
{
|
||||
for key in mime_message.gossiped_keys.values() {
|
||||
if key.public_key.dc_fingerprint() == self_fingerprint {
|
||||
self_found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
private_chat_id,
|
||||
&stock_str::securejoin_wait(context).await,
|
||||
&stock_str::securejoin_wait(context),
|
||||
SystemMessage::SecurejoinWait,
|
||||
None,
|
||||
time(),
|
||||
|
||||
@@ -109,10 +109,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let mut i = 0..msg_cnt;
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), messages_e2ee_info_msg(&bob).await);
|
||||
assert_eq!(msg.get_text(), messages_e2ee_info_msg(&bob));
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob));
|
||||
|
||||
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
@@ -250,7 +250,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = messages_e2ee_info_msg(&alice).await;
|
||||
let expected_text = messages_e2ee_info_msg(&alice);
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
// The `SecurejoinWait` info message has been removed, but the e2ee notice remains.
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), messages_e2ee_info_msg(&bob).await);
|
||||
assert_eq!(msg.get_text(), messages_e2ee_info_msg(&bob));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -625,7 +625,7 @@ async fn test_secure_join_group_ex(v3: bool, remove_invite: bool) -> Result<()>
|
||||
// - You added member bob@example.net
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = messages_e2ee_info_msg(&alice).await;
|
||||
let expected_text = messages_e2ee_info_msg(&alice);
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,12 +34,10 @@ pub(crate) fn remove_message_footer<'a>(
|
||||
// some providers encode `-- ` to `=2D-` which results in only `--`;
|
||||
// use that only when no other footer is found
|
||||
// and if the line before is empty and the line after is not empty
|
||||
"--" => {
|
||||
if (ix == 0 || lines.get(ix.saturating_sub(1)).is_none_or_empty())
|
||||
&& !lines.get(ix + 1).is_none_or_empty()
|
||||
{
|
||||
nearly_standard_footer = Some(ix);
|
||||
}
|
||||
"--" if (ix == 0 || lines.get(ix.saturating_sub(1)).is_none_or_empty())
|
||||
&& !lines.get(ix + 1).is_none_or_empty() =>
|
||||
{
|
||||
nearly_standard_footer = Some(ix);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
18
src/smtp.rs
18
src/smtp.rs
@@ -465,11 +465,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
match status {
|
||||
SendResult::Retry => Err(format_err!("Retry")),
|
||||
SendResult::Success => {
|
||||
if !context
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
|
||||
.await?
|
||||
{
|
||||
if !msg_has_pending_smtp_job(context, msg_id).await? {
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -478,6 +474,16 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn msg_has_pending_smtp_job(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
) -> Result<bool, Error> {
|
||||
context
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Attempts to send queued MDNs.
|
||||
async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
|
||||
loop {
|
||||
@@ -706,7 +712,7 @@ pub(crate) async fn add_self_recipients(
|
||||
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||
// messages.
|
||||
if encrypted {
|
||||
for addr in context.get_secondary_self_addrs().await? {
|
||||
for addr in context.get_published_secondary_self_addrs().await? {
|
||||
recipients.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/sql.rs
20
src/sql.rs
@@ -883,6 +883,24 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
// Cleanup `imap` and `imap_sync` entries for deleted transports.
|
||||
//
|
||||
// Transports may be deleted directly or via sync messages,
|
||||
// so it is easier to cleanup orphaned entries in a single place.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE transport_id NOT IN (SELECT transports.id FROM transports)",
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
context.sql.execute(
|
||||
"DELETE FROM imap_sync WHERE transport_id NOT IN (SELECT transports.id FROM transports)",
|
||||
(),
|
||||
).await.log_err(context).ok();
|
||||
|
||||
// Delete POI locations
|
||||
// which don't have corresponding message.
|
||||
delete_orphaned_poi_locations(context)
|
||||
@@ -901,7 +919,7 @@ async fn maybe_add_mvbox_move_deprecation_message(context: &Context) -> Result<(
|
||||
if !context.get_config_bool(Config::OnlyFetchMvbox).await?
|
||||
&& context.get_config_bool(Config::MvboxMove).await?
|
||||
{
|
||||
let mut msg = Message::new_text(stock_str::mvbox_move_deprecation(context).await);
|
||||
let mut msg = Message::new_text(stock_str::mvbox_move_deprecation(context));
|
||||
add_device_msg(context, Some("mvbox_move_deprecation"), Some(&mut msg)).await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -16,7 +16,6 @@ use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::key::DcKey;
|
||||
use crate::log::warn;
|
||||
use crate::message::MsgId;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::{Time, inc_and_check, time_elapsed};
|
||||
@@ -734,12 +733,6 @@ impl Sql {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_db_version_in_cache(&self, version: i32) -> Result<()> {
|
||||
let mut lock = self.config_cache.write().await;
|
||||
lock.insert(VERSION_CFG.to_string(), Some(format!("{version}")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_migration(&self, query: &str, version: i32) -> Result<()> {
|
||||
self.execute_migration_transaction(
|
||||
|transaction| {
|
||||
@@ -1612,51 +1605,11 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
if dbversion < 108 {
|
||||
let version = 108;
|
||||
let chunk_size = context.get_max_smtp_rcpt_to().await?;
|
||||
sql.transaction(move |trans| {
|
||||
Sql::set_db_version_trans(trans, version)?;
|
||||
let id_max =
|
||||
trans.query_row("SELECT IFNULL((SELECT MAX(id) FROM smtp), 0)", (), |row| {
|
||||
let id_max: i64 = row.get(0)?;
|
||||
Ok(id_max)
|
||||
})?;
|
||||
while let Some((id, rfc724_mid, mime, msg_id, recipients, retries)) = trans
|
||||
.query_row(
|
||||
"SELECT id, rfc724_mid, mime, msg_id, recipients, retries FROM smtp \
|
||||
WHERE id<=? LIMIT 1",
|
||||
(id_max,),
|
||||
|row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let rfc724_mid: String = row.get(1)?;
|
||||
let mime: String = row.get(2)?;
|
||||
let msg_id: MsgId = row.get(3)?;
|
||||
let recipients: String = row.get(4)?;
|
||||
let retries: i64 = row.get(5)?;
|
||||
Ok((id, rfc724_mid, mime, msg_id, recipients, retries))
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
{
|
||||
trans.execute("DELETE FROM smtp WHERE id=?", (id,))?;
|
||||
let recipients = recipients.split(' ').collect::<Vec<_>>();
|
||||
for recipients in recipients.chunks(chunk_size) {
|
||||
let recipients = recipients.join(" ");
|
||||
trans.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, mime, msg_id, recipients, retries) \
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
(&rfc724_mid, &mime, msg_id, recipients, retries),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("migration failed for version {version}"))?;
|
||||
|
||||
sql.set_db_version_in_cache(version).await?;
|
||||
}
|
||||
// Migration 108 is removed, it was using high level code
|
||||
// to split SMTP queue messages into chunks with smaller number of recipients
|
||||
// and started to fail later as high level code started
|
||||
// expecting `transports` table that is only added in future migrations.
|
||||
// Migration 108 was not changing the database schema.
|
||||
|
||||
if dbversion < 109 {
|
||||
sql.execute_migration(
|
||||
@@ -2343,6 +2296,26 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Add an `is_published` flag to transports.
|
||||
// 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.
|
||||
// The default is true, but when when the user updates the app,
|
||||
// existing secondary transports are set to unpublished,
|
||||
// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
inc_and_check(&mut migration_version, 149)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE transports ADD COLUMN is_published INTEGER DEFAULT 1 NOT NULL;
|
||||
UPDATE transports SET is_published=0 WHERE addr!=(
|
||||
SELECT value FROM config WHERE keyname='configured_addr'
|
||||
)",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -71,24 +71,6 @@ struct InnerPool {
|
||||
/// This mutex is locked when write connection
|
||||
/// is outside the pool.
|
||||
pub(crate) write_mutex: Arc<Mutex<()>>,
|
||||
|
||||
/// WAL checkpointing mutex.
|
||||
///
|
||||
/// This mutex ensures that no more than one thread
|
||||
/// runs WAL checkpointing at the same time.
|
||||
///
|
||||
/// Normal procedures acquire either one read connection
|
||||
/// or one write connection with a write mutex,
|
||||
/// and return the resources without trying to acquire
|
||||
/// more connections or trying to acquire write mutex
|
||||
/// without returning the read connection first.
|
||||
/// WAL checkpointing is special, it tries to acquire all
|
||||
/// connections and the write mutex,
|
||||
/// so two threads doing this at the same time
|
||||
/// may result in a deadlock with one thread
|
||||
/// waiting for a write lock and the other thread
|
||||
/// waiting for a connection.
|
||||
wal_checkpoint_mutex: Mutex<()>,
|
||||
}
|
||||
|
||||
impl InnerPool {
|
||||
@@ -209,7 +191,6 @@ impl Pool {
|
||||
connections: parking_lot::Mutex::new(connections),
|
||||
semaphore,
|
||||
write_mutex: Default::default(),
|
||||
wal_checkpoint_mutex: Default::default(),
|
||||
});
|
||||
Pool { inner }
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ pub(crate) struct WalCheckpointStats {
|
||||
|
||||
/// Runs a checkpoint operation in TRUNCATE mode, so the WAL file is truncated to 0 bytes.
|
||||
pub(super) async fn wal_checkpoint(pool: &Pool) -> Result<WalCheckpointStats> {
|
||||
let _guard = pool.inner.wal_checkpoint_mutex.lock().await;
|
||||
let t_start = Time::now();
|
||||
|
||||
// Do as much work as possible without blocking anybody.
|
||||
@@ -48,22 +47,25 @@ pub(super) async fn wal_checkpoint(pool: &Pool) -> Result<WalCheckpointStats> {
|
||||
conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |_| Ok(()))
|
||||
})?;
|
||||
|
||||
// Kick out writers.
|
||||
const _: () = assert!(Sql::N_DB_CONNECTIONS > 1, "Deadlock possible");
|
||||
// Kick out writers. `write_mutex` should be locked before taking an `InnerPool.semaphore`
|
||||
// permit to avoid ABBA deadlocks, so drop `conn` which holds a semaphore permit.
|
||||
drop(conn);
|
||||
let _write_lock = Arc::clone(&pool.inner.write_mutex).lock_owned().await;
|
||||
let t_writers_blocked = Time::now();
|
||||
let conn = pool.get(query_only).await?;
|
||||
// Ensure that all readers use the most recent database snapshot (are at the end of WAL) so
|
||||
// that `wal_checkpoint(FULL)` isn't blocked. We could use `PASSIVE` as well, but it's
|
||||
// documented poorly, https://www.sqlite.org/pragma.html#pragma_wal_checkpoint and
|
||||
// https://www.sqlite.org/c3ref/wal_checkpoint_v2.html don't tell how it interacts with new
|
||||
// readers.
|
||||
let mut read_conns = Vec::with_capacity(crate::sql::Sql::N_DB_CONNECTIONS - 1);
|
||||
for _ in 0..(crate::sql::Sql::N_DB_CONNECTIONS - 1) {
|
||||
let mut read_conns = Vec::with_capacity(Sql::N_DB_CONNECTIONS - 1);
|
||||
for _ in 0..(Sql::N_DB_CONNECTIONS - 1) {
|
||||
read_conns.push(pool.get(query_only).await?);
|
||||
}
|
||||
read_conns.clear();
|
||||
// Checkpoint the remaining WAL pages without blocking readers.
|
||||
let (pages_total, pages_checkpointed) = tokio::task::block_in_place(|| {
|
||||
conn.query_row("PRAGMA table_list", [], |_| Ok(()))?;
|
||||
conn.query_row("PRAGMA wal_checkpoint(FULL)", [], |row| {
|
||||
let pages_total: i64 = row.get(1)?;
|
||||
let pages_checkpointed: i64 = row.get(2)?;
|
||||
@@ -71,7 +73,7 @@ pub(super) async fn wal_checkpoint(pool: &Pool) -> Result<WalCheckpointStats> {
|
||||
})
|
||||
})?;
|
||||
// Kick out readers to avoid blocking/SQLITE_BUSY.
|
||||
for _ in 0..(crate::sql::Sql::N_DB_CONNECTIONS - 1) {
|
||||
for _ in 0..(Sql::N_DB_CONNECTIONS - 1) {
|
||||
read_conns.push(pool.get(query_only).await?);
|
||||
}
|
||||
let t_readers_blocked = Time::now();
|
||||
|
||||
@@ -281,7 +281,7 @@ async fn send_stats(context: &Context) -> Result<ChatId> {
|
||||
let chat_id = get_stats_chat_id(context).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text(crate::stock_str::stats_msg_body(context).await);
|
||||
msg.set_text(crate::stock_str::stats_msg_body(context));
|
||||
|
||||
let stats = get_stats(context).await?;
|
||||
|
||||
|
||||
414
src/stock_str.rs
414
src/stock_str.rs
@@ -4,9 +4,9 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use parking_lot::RwLock;
|
||||
use strum::EnumProperty as EnumPropertyTrait;
|
||||
use strum_macros::EnumProperty;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::accounts::Accounts;
|
||||
use crate::blob::BlobObject;
|
||||
@@ -463,17 +463,16 @@ impl StockStrings {
|
||||
}
|
||||
}
|
||||
|
||||
async fn translated(&self, id: StockMessage) -> String {
|
||||
fn translated(&self, id: StockMessage) -> String {
|
||||
self.translated_stockstrings
|
||||
.read()
|
||||
.await
|
||||
.get(&(id as usize))
|
||||
.map(AsRef::as_ref)
|
||||
.unwrap_or_else(|| id.fallback())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
|
||||
fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
|
||||
if stockstring.contains("%1") && !id.fallback().contains("%1") {
|
||||
bail!(
|
||||
"translation {} contains invalid %1 placeholder, default is {}",
|
||||
@@ -490,14 +489,13 @@ impl StockStrings {
|
||||
}
|
||||
self.translated_stockstrings
|
||||
.write()
|
||||
.await
|
||||
.insert(id as usize, stockstring);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn translated(context: &Context, id: StockMessage) -> String {
|
||||
context.translated_stockstrings.translated(id).await
|
||||
fn translated(context: &Context, id: StockMessage) -> String {
|
||||
context.translated_stockstrings.translated(id)
|
||||
}
|
||||
|
||||
/// Helper trait only meant to be implemented for [`String`].
|
||||
@@ -546,43 +544,43 @@ impl ContactId {
|
||||
impl StockStringMods for String {}
|
||||
|
||||
/// Stock string: `No messages.`.
|
||||
pub(crate) async fn no_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::NoMessages).await
|
||||
pub(crate) fn no_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::NoMessages)
|
||||
}
|
||||
|
||||
/// Stock string: `Me`.
|
||||
pub(crate) async fn self_msg(context: &Context) -> String {
|
||||
translated(context, StockMessage::SelfMsg).await
|
||||
pub(crate) fn self_msg(context: &Context) -> String {
|
||||
translated(context, StockMessage::SelfMsg)
|
||||
}
|
||||
|
||||
/// Stock string: `Draft`.
|
||||
pub(crate) async fn draft(context: &Context) -> String {
|
||||
translated(context, StockMessage::Draft).await
|
||||
pub(crate) fn draft(context: &Context) -> String {
|
||||
translated(context, StockMessage::Draft)
|
||||
}
|
||||
|
||||
/// Stock string: `Voice message`.
|
||||
pub(crate) async fn voice_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::VoiceMessage).await
|
||||
pub(crate) fn voice_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::VoiceMessage)
|
||||
}
|
||||
|
||||
/// Stock string: `Image`.
|
||||
pub(crate) async fn image(context: &Context) -> String {
|
||||
translated(context, StockMessage::Image).await
|
||||
pub(crate) fn image(context: &Context) -> String {
|
||||
translated(context, StockMessage::Image)
|
||||
}
|
||||
|
||||
/// Stock string: `Video`.
|
||||
pub(crate) async fn video(context: &Context) -> String {
|
||||
translated(context, StockMessage::Video).await
|
||||
pub(crate) fn video(context: &Context) -> String {
|
||||
translated(context, StockMessage::Video)
|
||||
}
|
||||
|
||||
/// Stock string: `Audio`.
|
||||
pub(crate) async fn audio(context: &Context) -> String {
|
||||
translated(context, StockMessage::Audio).await
|
||||
pub(crate) fn audio(context: &Context) -> String {
|
||||
translated(context, StockMessage::Audio)
|
||||
}
|
||||
|
||||
/// Stock string: `File`.
|
||||
pub(crate) async fn file(context: &Context) -> String {
|
||||
translated(context, StockMessage::File).await
|
||||
pub(crate) fn file(context: &Context) -> String {
|
||||
translated(context, StockMessage::File)
|
||||
}
|
||||
|
||||
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
|
||||
@@ -594,12 +592,10 @@ pub(crate) async fn msg_grp_name(
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouChangedGrpName)
|
||||
.await
|
||||
.replace1(from_group)
|
||||
.replace2(to_group)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgGrpNameChangedBy)
|
||||
.await
|
||||
.replace1(from_group)
|
||||
.replace2(to_group)
|
||||
.replace3(&by_contact.get_stock_name(context).await)
|
||||
@@ -608,10 +604,9 @@ pub(crate) async fn msg_grp_name(
|
||||
|
||||
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouChangedGrpImg).await
|
||||
translated(context, StockMessage::MsgYouChangedGrpImg)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgGrpImgChangedBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -621,10 +616,9 @@ pub(crate) async fn msg_chat_description_changed(
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouChangedDescription).await
|
||||
translated(context, StockMessage::MsgYouChangedDescription)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgChatDescriptionChangedBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -640,16 +634,11 @@ pub(crate) async fn msg_add_member_local(
|
||||
) -> String {
|
||||
let whom = added_member.get_stock_name(context).await;
|
||||
if by_contact == ContactId::UNDEFINED {
|
||||
translated(context, StockMessage::MsgAddMember)
|
||||
.await
|
||||
.replace1(&whom)
|
||||
translated(context, StockMessage::MsgAddMember).replace1(&whom)
|
||||
} else if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouAddMember)
|
||||
.await
|
||||
.replace1(&whom)
|
||||
translated(context, StockMessage::MsgYouAddMember).replace1(&whom)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgAddMemberBy)
|
||||
.await
|
||||
.replace1(&whom)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
@@ -666,16 +655,11 @@ pub(crate) async fn msg_del_member_local(
|
||||
) -> String {
|
||||
let whom = removed_member.get_stock_name(context).await;
|
||||
if by_contact == ContactId::UNDEFINED {
|
||||
translated(context, StockMessage::MsgDelMember)
|
||||
.await
|
||||
.replace1(&whom)
|
||||
translated(context, StockMessage::MsgDelMember).replace1(&whom)
|
||||
} else if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouDelMember)
|
||||
.await
|
||||
.replace1(&whom)
|
||||
translated(context, StockMessage::MsgYouDelMember).replace1(&whom)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgDelMemberBy)
|
||||
.await
|
||||
.replace1(&whom)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
@@ -684,22 +668,21 @@ pub(crate) async fn msg_del_member_local(
|
||||
/// Stock string: `You left the group.` or `Group left by %1$s.`.
|
||||
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouLeftGroup).await
|
||||
translated(context, StockMessage::MsgYouLeftGroup)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgGroupLeftBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `You left the channel.`
|
||||
pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgYouLeftBroadcast).await
|
||||
pub(crate) fn msg_you_left_broadcast(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgYouLeftBroadcast)
|
||||
}
|
||||
|
||||
/// Stock string: `You joined the channel.`
|
||||
pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgYouJoinedBroadcast).await
|
||||
pub(crate) fn msg_you_joined_broadcast(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgYouJoinedBroadcast)
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s invited you to join this channel. Waiting for the device of %2$s to reply…`.
|
||||
@@ -709,7 +692,6 @@ pub(crate) async fn secure_join_broadcast_started(
|
||||
) -> String {
|
||||
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
|
||||
translated(context, StockMessage::SecureJoinBroadcastStarted)
|
||||
.await
|
||||
.replace1(contact.get_display_name())
|
||||
.replace2(contact.get_display_name())
|
||||
} else {
|
||||
@@ -718,16 +700,15 @@ pub(crate) async fn secure_join_broadcast_started(
|
||||
}
|
||||
|
||||
/// Stock string: `Channel name changed from "1%s" to "2$s".`
|
||||
pub(crate) async fn msg_broadcast_name_changed(context: &Context, from: &str, to: &str) -> String {
|
||||
pub(crate) fn msg_broadcast_name_changed(context: &Context, from: &str, to: &str) -> String {
|
||||
translated(context, StockMessage::MsgBroadcastNameChanged)
|
||||
.await
|
||||
.replace1(from)
|
||||
.replace2(to)
|
||||
}
|
||||
|
||||
/// Stock string `Channel image changed.`
|
||||
pub(crate) async fn msg_broadcast_img_changed(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgBroadcastImgChanged).await
|
||||
pub(crate) fn msg_broadcast_img_changed(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgBroadcastImgChanged)
|
||||
}
|
||||
|
||||
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
|
||||
@@ -739,12 +720,10 @@ pub(crate) async fn msg_reacted(
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouReacted)
|
||||
.await
|
||||
.replace1(reaction)
|
||||
.replace2(summary)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgReactedBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
.replace2(reaction)
|
||||
.replace3(summary)
|
||||
@@ -752,27 +731,26 @@ pub(crate) async fn msg_reacted(
|
||||
}
|
||||
|
||||
/// Stock string: `GIF`.
|
||||
pub(crate) async fn gif(context: &Context) -> String {
|
||||
translated(context, StockMessage::Gif).await
|
||||
pub(crate) fn gif(context: &Context) -> String {
|
||||
translated(context, StockMessage::Gif)
|
||||
}
|
||||
|
||||
/// Stock string: `No encryption.`.
|
||||
pub(crate) async fn encr_none(context: &Context) -> String {
|
||||
translated(context, StockMessage::EncrNone).await
|
||||
pub(crate) fn encr_none(context: &Context) -> String {
|
||||
translated(context, StockMessage::EncrNone)
|
||||
}
|
||||
|
||||
/// Stock string: `Fingerprints`.
|
||||
pub(crate) async fn finger_prints(context: &Context) -> String {
|
||||
translated(context, StockMessage::FingerPrints).await
|
||||
pub(crate) fn finger_prints(context: &Context) -> String {
|
||||
translated(context, StockMessage::FingerPrints)
|
||||
}
|
||||
|
||||
/// Stock string: `Group image deleted.`.
|
||||
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouDeletedGrpImg).await
|
||||
translated(context, StockMessage::MsgYouDeletedGrpImg)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgGrpImgDeletedBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -784,7 +762,6 @@ pub(crate) async fn secure_join_started(
|
||||
) -> String {
|
||||
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
|
||||
translated(context, StockMessage::SecureJoinStarted)
|
||||
.await
|
||||
.replace1(contact.get_display_name())
|
||||
.replace2(contact.get_display_name())
|
||||
} else {
|
||||
@@ -795,22 +772,21 @@ pub(crate) async fn secure_join_started(
|
||||
/// Stock string: `%1$s replied, waiting for being added to the group…`.
|
||||
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
|
||||
translated(context, StockMessage::SecureJoinReplies)
|
||||
.await
|
||||
.replace1(&contact_id.get_stock_name(context).await)
|
||||
}
|
||||
|
||||
/// Stock string: `Establishing connection, please wait…`.
|
||||
pub(crate) async fn securejoin_wait(context: &Context) -> String {
|
||||
translated(context, StockMessage::SecurejoinWait).await
|
||||
pub(crate) fn securejoin_wait(context: &Context) -> String {
|
||||
translated(context, StockMessage::SecurejoinWait)
|
||||
}
|
||||
|
||||
/// Stock string: `❤️ Seems you're enjoying Delta Chat!`…
|
||||
pub(crate) async fn donation_request(context: &Context) -> String {
|
||||
translated(context, StockMessage::DonationRequest).await
|
||||
pub(crate) fn donation_request(context: &Context) -> String {
|
||||
translated(context, StockMessage::DonationRequest)
|
||||
}
|
||||
|
||||
/// Stock string: `Outgoing video call` or `Outgoing audio call`.
|
||||
pub(crate) async fn outgoing_call(context: &Context, has_video: bool) -> String {
|
||||
pub(crate) fn outgoing_call(context: &Context, has_video: bool) -> String {
|
||||
translated(
|
||||
context,
|
||||
if has_video {
|
||||
@@ -819,11 +795,10 @@ pub(crate) async fn outgoing_call(context: &Context, has_video: bool) -> String
|
||||
StockMessage::OutgoingAudioCall
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Stock string: `Incoming video call` or `Incoming audio call`.
|
||||
pub(crate) async fn incoming_call(context: &Context, has_video: bool) -> String {
|
||||
pub(crate) fn incoming_call(context: &Context, has_video: bool) -> String {
|
||||
translated(
|
||||
context,
|
||||
if has_video {
|
||||
@@ -832,26 +807,25 @@ pub(crate) async fn incoming_call(context: &Context, has_video: bool) -> String
|
||||
StockMessage::IncomingAudioCall
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Stock string: `Declined call`.
|
||||
pub(crate) async fn declined_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::DeclinedCall).await
|
||||
pub(crate) fn declined_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::DeclinedCall)
|
||||
}
|
||||
|
||||
/// Stock string: `Canceled call`.
|
||||
pub(crate) async fn canceled_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::CanceledCall).await
|
||||
pub(crate) fn canceled_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::CanceledCall)
|
||||
}
|
||||
|
||||
/// Stock string: `Missed call`.
|
||||
pub(crate) async fn missed_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::MissedCall).await
|
||||
pub(crate) fn missed_call(context: &Context) -> String {
|
||||
translated(context, StockMessage::MissedCall)
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to chat with %1$s`.
|
||||
pub(crate) async fn setup_contact_qr_description(
|
||||
pub(crate) fn setup_contact_qr_description(
|
||||
context: &Context,
|
||||
display_name: &str,
|
||||
addr: &str,
|
||||
@@ -861,113 +835,100 @@ pub(crate) async fn setup_contact_qr_description(
|
||||
} else {
|
||||
display_name.to_owned()
|
||||
};
|
||||
translated(context, StockMessage::SetupContactQRDescription)
|
||||
.await
|
||||
.replace1(&name)
|
||||
translated(context, StockMessage::SetupContactQRDescription).replace1(&name)
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to join group %1$s`.
|
||||
pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
|
||||
translated(context, StockMessage::SecureJoinGroupQRDescription)
|
||||
.await
|
||||
.replace1(chat.get_name())
|
||||
pub(crate) fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
|
||||
translated(context, StockMessage::SecureJoinGroupQRDescription).replace1(chat.get_name())
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to join channel %1$s`.
|
||||
pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
|
||||
translated(context, StockMessage::SecureJoinBrodcastQRDescription)
|
||||
.await
|
||||
.replace1(chat.get_name())
|
||||
pub(crate) fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
|
||||
translated(context, StockMessage::SecureJoinBrodcastQRDescription).replace1(chat.get_name())
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
|
||||
pub(crate) fn contact_verified(context: &Context, contact: &Contact) -> String {
|
||||
let addr = contact.get_display_name();
|
||||
translated(context, StockMessage::ContactVerified)
|
||||
.await
|
||||
.replace1(addr)
|
||||
translated(context, StockMessage::ContactVerified).replace1(addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Archived chats`.
|
||||
pub(crate) async fn archived_chats(context: &Context) -> String {
|
||||
translated(context, StockMessage::ArchivedChats).await
|
||||
pub(crate) fn archived_chats(context: &Context) -> String {
|
||||
translated(context, StockMessage::ArchivedChats)
|
||||
}
|
||||
|
||||
/// Stock string: `Multi Device Synchronization`.
|
||||
pub(crate) async fn sync_msg_subject(context: &Context) -> String {
|
||||
translated(context, StockMessage::SyncMsgSubject).await
|
||||
pub(crate) fn sync_msg_subject(context: &Context) -> String {
|
||||
translated(context, StockMessage::SyncMsgSubject)
|
||||
}
|
||||
|
||||
/// Stock string: `This message is used to synchronize data between your devices.`.
|
||||
pub(crate) async fn sync_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::SyncMsgBody).await
|
||||
pub(crate) fn sync_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::SyncMsgBody)
|
||||
}
|
||||
|
||||
/// Stock string: `Cannot login as \"%1$s\". Please check...`.
|
||||
pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
|
||||
translated(context, StockMessage::CannotLogin)
|
||||
.await
|
||||
.replace1(user)
|
||||
pub(crate) fn cannot_login(context: &Context, user: &str) -> String {
|
||||
translated(context, StockMessage::CannotLogin).replace1(user)
|
||||
}
|
||||
|
||||
/// Stock string: `Location streaming enabled.`.
|
||||
pub(crate) async fn msg_location_enabled(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgLocationEnabled).await
|
||||
pub(crate) fn msg_location_enabled(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgLocationEnabled)
|
||||
}
|
||||
|
||||
/// Stock string: `Location streaming enabled by ...`.
|
||||
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
|
||||
if contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEnabledLocation).await
|
||||
translated(context, StockMessage::MsgYouEnabledLocation)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgLocationEnabledBy)
|
||||
.await
|
||||
.replace1(&contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Location streaming disabled.`.
|
||||
pub(crate) async fn msg_location_disabled(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgLocationDisabled).await
|
||||
pub(crate) fn msg_location_disabled(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgLocationDisabled)
|
||||
}
|
||||
|
||||
/// Stock string: `Location`.
|
||||
pub(crate) async fn location(context: &Context) -> String {
|
||||
translated(context, StockMessage::Location).await
|
||||
pub(crate) fn location(context: &Context) -> String {
|
||||
translated(context, StockMessage::Location)
|
||||
}
|
||||
|
||||
/// Stock string: `Sticker`.
|
||||
pub(crate) async fn sticker(context: &Context) -> String {
|
||||
translated(context, StockMessage::Sticker).await
|
||||
pub(crate) fn sticker(context: &Context) -> String {
|
||||
translated(context, StockMessage::Sticker)
|
||||
}
|
||||
|
||||
/// Stock string: `Device messages`.
|
||||
pub(crate) async fn device_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::DeviceMessages).await
|
||||
pub(crate) fn device_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::DeviceMessages)
|
||||
}
|
||||
|
||||
/// Stock string: `Saved messages`.
|
||||
pub(crate) async fn saved_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::SavedMessages).await
|
||||
pub(crate) fn saved_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::SavedMessages)
|
||||
}
|
||||
|
||||
/// Stock string: `Messages in this chat are generated locally by...`.
|
||||
pub(crate) async fn device_messages_hint(context: &Context) -> String {
|
||||
translated(context, StockMessage::DeviceMessagesHint).await
|
||||
pub(crate) fn device_messages_hint(context: &Context) -> String {
|
||||
translated(context, StockMessage::DeviceMessagesHint)
|
||||
}
|
||||
|
||||
/// Stock string: `Welcome to Delta Chat! – ...`.
|
||||
pub(crate) async fn welcome_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::WelcomeMessage).await
|
||||
pub(crate) fn welcome_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::WelcomeMessage)
|
||||
}
|
||||
|
||||
/// Stock string: `Message from %1$s`.
|
||||
// TODO: This can compute `self_name` itself instead of asking the caller to do this.
|
||||
pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
|
||||
translated(context, StockMessage::SubjectForNewContact)
|
||||
.await
|
||||
.replace1(self_name)
|
||||
pub(crate) fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
|
||||
translated(context, StockMessage::SubjectForNewContact).replace1(self_name)
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is disabled.`.
|
||||
@@ -976,10 +937,9 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
|
||||
translated(context, StockMessage::MsgYouDisabledEphemeralTimer)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -991,12 +951,9 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
|
||||
.await
|
||||
.replace1(timer)
|
||||
translated(context, StockMessage::MsgYouEnabledEphemeralTimer).replace1(timer)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
|
||||
.await
|
||||
.replace1(timer)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
@@ -1005,10 +962,9 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
|
||||
/// Stock string: `Message deletion timer is set to 1 hour.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerHour).await
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerHour)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerHourBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -1016,10 +972,9 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
|
||||
/// Stock string: `Message deletion timer is set to 1 day.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerDay).await
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerDay)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDayBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -1027,10 +982,9 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
|
||||
/// Stock string: `Message deletion timer is set to 1 week.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerWeek)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerWeekBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
@@ -1038,57 +992,52 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
|
||||
/// Stock string: `Message deletion timer is set to 1 year.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerYear).await
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerYear)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerYearBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Error:\n\n“%1$s”`.
|
||||
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
|
||||
translated(context, StockMessage::ConfigurationFailed)
|
||||
.await
|
||||
.replace1(details)
|
||||
pub(crate) fn configuration_failed(context: &Context, details: &str) -> String {
|
||||
translated(context, StockMessage::ConfigurationFailed).replace1(details)
|
||||
}
|
||||
|
||||
/// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`.
|
||||
// TODO: This could compute now itself.
|
||||
pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
|
||||
translated(context, StockMessage::BadTimeMsgBody)
|
||||
.await
|
||||
.replace1(now)
|
||||
pub(crate) fn bad_time_msg_body(context: &Context, now: &str) -> String {
|
||||
translated(context, StockMessage::BadTimeMsgBody).replace1(now)
|
||||
}
|
||||
|
||||
/// Stock string: `⚠️ Your Delta Chat version might be outdated...`.
|
||||
pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::UpdateReminderMsgBody).await
|
||||
pub(crate) fn update_reminder_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::UpdateReminderMsgBody)
|
||||
}
|
||||
|
||||
/// Stock string: `Could not find your mail server...`.
|
||||
pub(crate) async fn error_no_network(context: &Context) -> String {
|
||||
translated(context, StockMessage::ErrorNoNetwork).await
|
||||
pub(crate) fn error_no_network(context: &Context) -> String {
|
||||
translated(context, StockMessage::ErrorNoNetwork)
|
||||
}
|
||||
|
||||
/// Stock string: `Messages are end-to-end encrypted.`, used in info-messages, UI may add smth. as `Tap to learn more.`
|
||||
pub(crate) async fn messages_e2ee_info_msg(context: &Context) -> String {
|
||||
translated(context, StockMessage::ChatProtectionEnabled).await
|
||||
pub(crate) fn messages_e2ee_info_msg(context: &Context) -> String {
|
||||
translated(context, StockMessage::ChatProtectionEnabled)
|
||||
}
|
||||
|
||||
/// Stock string: `Messages are end-to-end encrypted.`
|
||||
pub(crate) async fn messages_are_e2ee(context: &Context) -> String {
|
||||
translated(context, StockMessage::MessagesAreE2ee).await
|
||||
pub(crate) fn messages_are_e2ee(context: &Context) -> String {
|
||||
translated(context, StockMessage::MessagesAreE2ee)
|
||||
}
|
||||
|
||||
/// Stock string: `Reply`.
|
||||
pub(crate) async fn reply_noun(context: &Context) -> String {
|
||||
translated(context, StockMessage::ReplyNoun).await
|
||||
pub(crate) fn reply_noun(context: &Context) -> String {
|
||||
translated(context, StockMessage::ReplyNoun)
|
||||
}
|
||||
|
||||
/// Stock string: `You deleted the \"Saved messages\" chat...`.
|
||||
pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::SelfDeletedMsgBody).await
|
||||
pub(crate) fn self_deleted_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::SelfDeletedMsgBody)
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to %1$s minutes.`.
|
||||
@@ -1098,12 +1047,9 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
|
||||
.await
|
||||
.replace1(minutes)
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerMinutes).replace1(minutes)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
|
||||
.await
|
||||
.replace1(minutes)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
@@ -1116,12 +1062,9 @@ pub(crate) async fn msg_ephemeral_timer_hours(
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerHours)
|
||||
.await
|
||||
.replace1(hours)
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerHours).replace1(hours)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
|
||||
.await
|
||||
.replace1(hours)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
@@ -1134,12 +1077,9 @@ pub(crate) async fn msg_ephemeral_timer_days(
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerDays)
|
||||
.await
|
||||
.replace1(days)
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerDays).replace1(days)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
|
||||
.await
|
||||
.replace1(days)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
@@ -1152,112 +1092,103 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
|
||||
.await
|
||||
.replace1(weeks)
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerWeeks).replace1(weeks)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
|
||||
.await
|
||||
.replace1(weeks)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Forwarded`.
|
||||
pub(crate) async fn forwarded(context: &Context) -> String {
|
||||
translated(context, StockMessage::Forwarded).await
|
||||
pub(crate) fn forwarded(context: &Context) -> String {
|
||||
translated(context, StockMessage::Forwarded)
|
||||
}
|
||||
|
||||
/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
|
||||
pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
|
||||
pub(crate) fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
|
||||
translated(context, StockMessage::QuotaExceedingMsgBody)
|
||||
.await
|
||||
.replace1(&format!("{highest_usage}"))
|
||||
.replace("%%", "%")
|
||||
}
|
||||
|
||||
/// Stock string: `Incoming Messages`.
|
||||
pub(crate) async fn incoming_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::IncomingMessages).await
|
||||
pub(crate) fn incoming_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::IncomingMessages)
|
||||
}
|
||||
|
||||
/// Stock string: `Outgoing Messages`.
|
||||
pub(crate) async fn outgoing_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::OutgoingMessages).await
|
||||
pub(crate) fn outgoing_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::OutgoingMessages)
|
||||
}
|
||||
|
||||
/// Stock string: `Not connected`.
|
||||
pub(crate) async fn not_connected(context: &Context) -> String {
|
||||
translated(context, StockMessage::NotConnected).await
|
||||
pub(crate) fn not_connected(context: &Context) -> String {
|
||||
translated(context, StockMessage::NotConnected)
|
||||
}
|
||||
|
||||
/// Stock string: `Connected`.
|
||||
pub(crate) async fn connected(context: &Context) -> String {
|
||||
translated(context, StockMessage::Connected).await
|
||||
pub(crate) fn connected(context: &Context) -> String {
|
||||
translated(context, StockMessage::Connected)
|
||||
}
|
||||
|
||||
/// Stock string: `Connecting…`.
|
||||
pub(crate) async fn connecting(context: &Context) -> String {
|
||||
translated(context, StockMessage::Connecting).await
|
||||
pub(crate) fn connecting(context: &Context) -> String {
|
||||
translated(context, StockMessage::Connecting)
|
||||
}
|
||||
|
||||
/// Stock string: `Updating…`.
|
||||
pub(crate) async fn updating(context: &Context) -> String {
|
||||
translated(context, StockMessage::Updating).await
|
||||
pub(crate) fn updating(context: &Context) -> String {
|
||||
translated(context, StockMessage::Updating)
|
||||
}
|
||||
|
||||
/// Stock string: `Sending…`.
|
||||
pub(crate) async fn sending(context: &Context) -> String {
|
||||
translated(context, StockMessage::Sending).await
|
||||
pub(crate) fn sending(context: &Context) -> String {
|
||||
translated(context, StockMessage::Sending)
|
||||
}
|
||||
|
||||
/// Stock string: `Your last message was sent successfully.`.
|
||||
pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
|
||||
translated(context, StockMessage::LastMsgSentSuccessfully).await
|
||||
pub(crate) fn last_msg_sent_successfully(context: &Context) -> String {
|
||||
translated(context, StockMessage::LastMsgSentSuccessfully)
|
||||
}
|
||||
|
||||
/// Stock string: `Error: %1$s…`.
|
||||
/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
|
||||
pub(crate) async fn error(context: &Context, error: &str) -> String {
|
||||
translated(context, StockMessage::Error)
|
||||
.await
|
||||
.replace1(error)
|
||||
pub(crate) fn error(context: &Context, error: &str) -> String {
|
||||
translated(context, StockMessage::Error).replace1(error)
|
||||
}
|
||||
|
||||
/// Stock string: `Not supported by your provider.`.
|
||||
pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
|
||||
translated(context, StockMessage::NotSupportedByProvider).await
|
||||
pub(crate) fn not_supported_by_provider(context: &Context) -> String {
|
||||
translated(context, StockMessage::NotSupportedByProvider)
|
||||
}
|
||||
|
||||
/// Stock string: `Messages`.
|
||||
/// Used as a subtitle in quota context; can be plural always.
|
||||
pub(crate) async fn messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::Messages).await
|
||||
pub(crate) fn messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::Messages)
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s of %2$s used`.
|
||||
pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
|
||||
pub(crate) fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
|
||||
translated(context, StockMessage::PartOfTotallUsed)
|
||||
.await
|
||||
.replace1(part)
|
||||
.replace2(total)
|
||||
}
|
||||
|
||||
/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
|
||||
pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
|
||||
translated(context, StockMessage::InvalidUnencryptedMail)
|
||||
.await
|
||||
.replace1(provider)
|
||||
translated(context, StockMessage::InvalidUnencryptedMail).replace1(provider)
|
||||
}
|
||||
|
||||
/// Stock string: `The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!`
|
||||
pub(crate) async fn stats_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::StatsMsgBody).await
|
||||
pub(crate) fn stats_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::StatsMsgBody)
|
||||
}
|
||||
|
||||
/// Stock string: `Others will only see this group after you sent a first message.`.
|
||||
pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::NewGroupSendFirstMessage).await
|
||||
pub(crate) fn new_group_send_first_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::NewGroupSendFirstMessage)
|
||||
}
|
||||
|
||||
/// Text to put in the [`Qr::Backup2`] rendered SVG image.
|
||||
@@ -1272,46 +1203,44 @@ pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
|
||||
} else {
|
||||
context.get_primary_self_addr().await?
|
||||
};
|
||||
Ok(translated(context, StockMessage::BackupTransferQr)
|
||||
.await
|
||||
.replace1(&name))
|
||||
Ok(translated(context, StockMessage::BackupTransferQr).replace1(&name))
|
||||
}
|
||||
|
||||
pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::BackupTransferMsgBody).await
|
||||
pub(crate) fn backup_transfer_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::BackupTransferMsgBody)
|
||||
}
|
||||
|
||||
/// Stock string: `Proxy Enabled`.
|
||||
pub(crate) async fn proxy_enabled(context: &Context) -> String {
|
||||
translated(context, StockMessage::ProxyEnabled).await
|
||||
pub(crate) fn proxy_enabled(context: &Context) -> String {
|
||||
translated(context, StockMessage::ProxyEnabled)
|
||||
}
|
||||
|
||||
/// Stock string: `You are using a proxy. If you're having trouble connecting, try a different proxy.`.
|
||||
pub(crate) async fn proxy_description(context: &Context) -> String {
|
||||
translated(context, StockMessage::ProxyEnabledDescription).await
|
||||
pub(crate) fn proxy_description(context: &Context) -> String {
|
||||
translated(context, StockMessage::ProxyEnabledDescription)
|
||||
}
|
||||
|
||||
/// Stock string: `Messages in this chat use classic email and are not encrypted.`.
|
||||
pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
|
||||
translated(context, StockMessage::ChatUnencryptedExplanation).await
|
||||
pub(crate) fn chat_unencrypted_explanation(context: &Context) -> String {
|
||||
translated(context, StockMessage::ChatUnencryptedExplanation)
|
||||
}
|
||||
|
||||
/// Stock string: `You are using the legacy option "Move automatically to DeltaChat Folder`…
|
||||
pub(crate) async fn mvbox_move_deprecation(context: &Context) -> String {
|
||||
translated(context, StockMessage::MvboxMoveDeprecation).await
|
||||
pub(crate) fn mvbox_move_deprecation(context: &Context) -> String {
|
||||
translated(context, StockMessage::MvboxMoveDeprecation)
|
||||
}
|
||||
|
||||
impl Viewtype {
|
||||
/// returns Localized name for message viewtype
|
||||
pub async fn to_locale_string(&self, context: &Context) -> String {
|
||||
pub fn to_locale_string(&self, context: &Context) -> String {
|
||||
match self {
|
||||
Viewtype::Image => image(context).await,
|
||||
Viewtype::Gif => gif(context).await,
|
||||
Viewtype::Sticker => sticker(context).await,
|
||||
Viewtype::Audio => audio(context).await,
|
||||
Viewtype::Voice => voice_message(context).await,
|
||||
Viewtype::Video => video(context).await,
|
||||
Viewtype::File => file(context).await,
|
||||
Viewtype::Image => image(context),
|
||||
Viewtype::Gif => gif(context),
|
||||
Viewtype::Sticker => sticker(context),
|
||||
Viewtype::Audio => audio(context),
|
||||
Viewtype::Voice => voice_message(context),
|
||||
Viewtype::Video => video(context),
|
||||
Viewtype::File => file(context),
|
||||
Viewtype::Webxdc => "Mini App".to_owned(),
|
||||
Viewtype::Vcard => "👤".to_string(),
|
||||
// The following shouldn't normally be shown to users, so translations aren't needed.
|
||||
@@ -1323,10 +1252,9 @@ impl Viewtype {
|
||||
impl Context {
|
||||
/// Set the stock string for the [StockMessage].
|
||||
///
|
||||
pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
|
||||
pub fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
|
||||
self.translated_stockstrings
|
||||
.set_stock_translation(id, stockstring)
|
||||
.await?;
|
||||
.set_stock_translation(id, stockstring)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1353,7 +1281,7 @@ impl Context {
|
||||
msg.param.set(Param::Filename, "welcome-image.jpg");
|
||||
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
|
||||
|
||||
let mut msg = Message::new_text(welcome_message(self).await);
|
||||
let mut msg = Message::new_text(welcome_message(self));
|
||||
chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1362,10 +1290,8 @@ impl Context {
|
||||
impl Accounts {
|
||||
/// Set the stock string for the [StockMessage].
|
||||
///
|
||||
pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
|
||||
self.stockstrings
|
||||
.set_stock_translation(id, stockstring)
|
||||
.await?;
|
||||
pub fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
|
||||
self.stockstrings.set_stock_translation(id, stockstring)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,8 @@ fn test_fallback() {
|
||||
async fn test_set_stock_translation() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(no_messages(&t).await, "xyz")
|
||||
assert_eq!(no_messages(&t), "xyz")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -31,13 +30,11 @@ async fn test_set_stock_translation_wrong_replacements() {
|
||||
assert!(
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
@@ -45,7 +42,7 @@ async fn test_set_stock_translation_wrong_replacements() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_str() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(no_messages(&t).await, "No messages.");
|
||||
assert_eq!(no_messages(&t), "No messages.");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -56,17 +53,14 @@ async fn test_stock_string_repl_str() {
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
|
||||
// uses %1$s substitution
|
||||
assert_eq!(contact_verified(&t, &contact).await, "Someone verified.");
|
||||
assert_eq!(contact_verified(&t, &contact), "Someone verified.");
|
||||
// We have no string using %1$d to test...
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_simple() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
msg_location_enabled(&t).await,
|
||||
"Location streaming enabled."
|
||||
)
|
||||
assert_eq!(msg_location_enabled(&t), "Location streaming enabled.")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -111,7 +105,7 @@ async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_exceeding_stock_str() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let str = quota_exceeding(&t, 81).await;
|
||||
let str = quota_exceeding(&t, 81);
|
||||
assert!(str.contains("81% "));
|
||||
assert!(str.contains("100% "));
|
||||
assert!(!str.contains("%%"));
|
||||
|
||||
@@ -98,13 +98,13 @@ impl Summary {
|
||||
contact: Option<&Contact>,
|
||||
) -> Result<Summary> {
|
||||
let prefix = if msg.state == MessageState::OutDraft {
|
||||
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
|
||||
Some(SummaryPrefix::Draft(stock_str::draft(context)))
|
||||
} else if msg.from_id == ContactId::SELF {
|
||||
if msg.is_info() || msg.viewtype == Viewtype::Call || chat.typ == Chattype::OutBroadcast
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
|
||||
Some(SummaryPrefix::Me(stock_str::self_msg(context)))
|
||||
}
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
@@ -124,7 +124,7 @@ impl Summary {
|
||||
let mut text = msg.get_summary_text(context).await;
|
||||
|
||||
if text.is_empty() && msg.quoted_text().is_some() {
|
||||
text = stock_str::reply_noun(context).await
|
||||
text = stock_str::reply_noun(context)
|
||||
}
|
||||
|
||||
let thumbnail_path = if msg.viewtype == Viewtype::Image
|
||||
@@ -160,7 +160,7 @@ impl Message {
|
||||
let summary = self.get_summary_text_without_prefix(context).await;
|
||||
|
||||
if self.is_forwarded() {
|
||||
format!("{}: {}", stock_str::forwarded(context).await, summary)
|
||||
format!("{}: {}", stock_str::forwarded(context), summary)
|
||||
} else {
|
||||
summary
|
||||
}
|
||||
@@ -180,43 +180,43 @@ impl Message {
|
||||
match viewtype {
|
||||
Viewtype::Image => {
|
||||
emoji = Some("📷");
|
||||
type_name = Some(stock_str::image(context).await);
|
||||
type_name = Some(stock_str::image(context));
|
||||
type_file = None;
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Gif => {
|
||||
emoji = None;
|
||||
type_name = Some(stock_str::gif(context).await);
|
||||
type_name = Some(stock_str::gif(context));
|
||||
type_file = None;
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Sticker => {
|
||||
emoji = None;
|
||||
type_name = Some(stock_str::sticker(context).await);
|
||||
type_name = Some(stock_str::sticker(context));
|
||||
type_file = None;
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Video => {
|
||||
emoji = Some("🎥");
|
||||
type_name = Some(stock_str::video(context).await);
|
||||
type_name = Some(stock_str::video(context));
|
||||
type_file = None;
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Voice => {
|
||||
emoji = Some("🎤");
|
||||
type_name = Some(stock_str::voice_message(context).await);
|
||||
type_name = Some(stock_str::voice_message(context));
|
||||
type_file = None;
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Audio => {
|
||||
emoji = Some("🎵");
|
||||
type_name = Some(stock_str::audio(context).await);
|
||||
type_name = Some(stock_str::audio(context));
|
||||
type_file = self.get_filename();
|
||||
append_text = true
|
||||
}
|
||||
Viewtype::File => {
|
||||
emoji = Some("📎");
|
||||
type_name = Some(stock_str::file(context).await);
|
||||
type_name = Some(stock_str::file(context));
|
||||
type_file = self.get_filename();
|
||||
append_text = true
|
||||
}
|
||||
@@ -255,14 +255,14 @@ impl Message {
|
||||
type_name = Some(match call_state {
|
||||
CallState::Alerting | CallState::Active | CallState::Completed { .. } => {
|
||||
if self.from_id == ContactId::SELF {
|
||||
stock_str::outgoing_call(context, has_video).await
|
||||
stock_str::outgoing_call(context, has_video)
|
||||
} else {
|
||||
stock_str::incoming_call(context, has_video).await
|
||||
stock_str::incoming_call(context, has_video)
|
||||
}
|
||||
}
|
||||
CallState::Missed => stock_str::missed_call(context).await,
|
||||
CallState::Declined => stock_str::declined_call(context).await,
|
||||
CallState::Canceled => stock_str::canceled_call(context).await,
|
||||
CallState::Missed => stock_str::missed_call(context),
|
||||
CallState::Declined => stock_str::declined_call(context),
|
||||
CallState::Canceled => stock_str::canceled_call(context),
|
||||
});
|
||||
type_file = None;
|
||||
append_text = false
|
||||
@@ -270,7 +270,7 @@ impl Message {
|
||||
Viewtype::Text | Viewtype::Unknown => {
|
||||
emoji = None;
|
||||
if self.param.get_cmd() == SystemMessage::LocationOnly {
|
||||
type_name = Some(stock_str::location(context).await);
|
||||
type_name = Some(stock_str::location(context));
|
||||
type_file = None;
|
||||
append_text = false;
|
||||
} else {
|
||||
@@ -490,12 +490,5 @@ mod tests {
|
||||
msg.get_summary_text_without_prefix(ctx).await,
|
||||
"📎 foo.bar \u{2013} bla bla"
|
||||
); // skipping prefix used for reactions summaries
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(ctx, "autocrypt-setup-message.html", b"data", None)
|
||||
.unwrap();
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
assert_summary_texts(&msg, ctx, "📎 autocrypt-setup-message.html").await;
|
||||
// no special handling of ASM
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user