Compare commits

..
Author SHA1 Message Date
link2xt 57d22f8406 feat: dc_accounts_set_push_device_token and dc_get_push_state APIs 2024-03-04 19:53:34 +00:00
link2xt 5fe6d0b106 build: increase MSRV to 1.74.0
This is what updated dependencies require.

Also update Rust used to build manylinux wheels
from 1.72.0 to 1.76.0.
2024-03-04 18:54:23 +00:00
link2xt d40673af1d chore: cargo update 2024-03-04 18:30:20 +00:00
link2xt a6d3ff6cf7 chore(deps): update mio to fix RUSTSEC-2024-0019 2024-03-04 18:26:18 +00:00
link2xt 15f3af418a chore(deps): update buffer-redux to remove unmaintained safemem 2024-03-04 18:25:37 +00:00
holger krekel 7cc2a09627 test: fix pytest compat (#5317)
seems pytest_report_header has changed with pytest incompatible but we
don't need it anyway so we can just leave it out.
2024-03-04 15:58:57 +00:00
link2xt c96139b579 build: do not vendor OpenSSL when cross-compiling (#5316)
Compilation of vendored OpenSSL inside Nix is broken since
<https://github.com/alexcrichton/openssl-src-rs/pull/229> due to build
script changes.

There is anyway no need to compile vendored OpenSSL as nixpkgs already
contains OpenSSL package.

This fixes `nix build .#deltachat-rpc-server-x86_64-linux` and similar
commands which are used during releases.
2024-03-04 15:57:17 +00:00
Hocuri c52998f2ff feat(Self-Reporting): Report number of protected/encrypted/unencrypted chats (#5292) 2024-03-04 11:03:37 +01:00
dependabot[bot] 12587684ca Merge pull request #5305 from deltachat/dependabot/cargo/tempfile-3.10.1 2024-03-03 20:44:06 +00:00
dependabot[bot] 0c1d578207 chore(cargo): bump tempfile from 3.10.0 to 3.10.1
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.10.0 to 3.10.1.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.10.0...v3.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-03 08:05:01 +00:00
101 changed files with 1669 additions and 6210 deletions
+6 -6
View File
@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.77.1
RUSTUP_TOOLCHAIN: 1.76.0
steps:
- uses: actions/checkout@v4
with:
@@ -83,15 +83,15 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.77.1
rust: 1.76.0
- os: windows-latest
rust: 1.77.1
rust: 1.76.0
- os: macos-latest
rust: 1.77.1
rust: 1.76.0
# Minimum Supported Rust Version = 1.77.0
# Minimum Supported Rust Version = 1.74.0
- os: ubuntu-latest
rust: 1.77.0
rust: 1.74.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
+18 -89
View File
@@ -21,7 +21,7 @@ jobs:
# Build a version statically linked against musl libc
# to avoid problems with glibc version incompatibility.
build_linux:
name: Linux
name: Build deltachat-rpc-server for Linux
strategy:
fail-fast: false
matrix:
@@ -45,7 +45,7 @@ jobs:
if-no-files-found: error
build_windows:
name: Windows
name: Build deltachat-rpc-server for Windows
strategy:
fail-fast: false
matrix:
@@ -69,7 +69,7 @@ jobs:
if-no-files-found: error
build_macos:
name: macOS
name: Build deltachat-rpc-server for macOS
strategy:
fail-fast: false
matrix:
@@ -94,46 +94,16 @@ jobs:
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
if-no-files-found: error
build_android:
name: Android
strategy:
fail-fast: false
matrix:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android
path: result/bin/deltachat-rpc-server
if-no-files-found: error
publish:
name: Build wheels and upload binaries to the release
needs: ["build_linux", "build_windows", "build_macos"]
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-server
needs: ["build_linux", "build_macos"]
permissions:
id-token: write
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
@@ -189,35 +159,18 @@ jobs:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: Create bin/ directory
- name: Flatten dist/ directory
run: |
mkdir -p bin
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-linux
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv7l-linux
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv6l-linux
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-i686-linux
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-linux
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
- name: List binaries
run: ls -l bin/
mkdir -p dist
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server dist/deltachat-rpc-server-armv7l-linux
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server dist/deltachat-rpc-server-armv6l-linux
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
@@ -229,29 +182,9 @@ jobs:
run: pip install wheel
- name: Build deltachat-rpc-server Python wheels and source package
run: |
mkdir -p dist
nix build .#deltachat-rpc-server-x86_64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv7l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv6l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-aarch64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-i686-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win64-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win32-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-source
cp result/*.tar.gz dist/
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
mv *.whl dist/
run: scripts/wheel-rpc-server.py
- name: List artifacts
- name: List downloaded artifacts
run: ls -l dist/
- name: Upload binaries to the GitHub release
@@ -261,8 +194,4 @@ jobs:
run: |
gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \
bin/* dist/*
- name: Publish deltachat-rpc-client to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
dist/*
@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: "18"
- name: Get tag
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
- name: Use Node.js 18.x
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Add Rust cache
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
show-progress: false
- name: Use Node.js 18.x
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18.x
+8 -8
View File
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: "18"
- name: System info
@@ -29,7 +29,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -37,7 +37,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/
@@ -78,7 +78,7 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: "18"
- run: apt-get update
@@ -99,7 +99,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -107,7 +107,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/
@@ -142,7 +142,7 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v2
with:
node-version: "18"
- name: Get tag
@@ -186,7 +186,7 @@ jobs:
tar -xvzf macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest.tar.gz -C node/prebuilds
tree node/prebuilds
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
rm -rf linux macos-latest windows-latest
- name: Install dependencies without running scripts
run: |
npm install --ignore-scripts
+3 -3
View File
@@ -26,7 +26,7 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: "18"
- name: System info
@@ -38,7 +38,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -46,7 +46,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/
@@ -1,47 +0,0 @@
name: Publish deltachat-rpc-client to PyPI
on:
workflow_dispatch:
release:
types: [published]
jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install pypa/build
run: python3 -m pip install build
- name: Build a binary wheel and a source tarball
working-directory: deltachat-rpc-client
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: deltachat-rpc-client/dist/
publish-to-pypi:
name: Publish Python distribution to PyPI
if: github.event_name == 'release'
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-client
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
-7
View File
@@ -44,10 +44,3 @@ node/build/
node/dist/
node/prebuilds/
node/.nyc_output/
# Nix symlink.
result
# direnv
.envrc
.direnv
+38 -293
View File
@@ -1,285 +1,5 @@
# Changelog
## [1.137.2] - 2024-04-05
### API-Changes
- [**breaking**] Increase Minimum Supported Rust Version to 1.77.0.
### Features / Changes
- Show reactions in summaries ([#5387](https://github.com/deltachat/deltachat-core-rust/pull/5387)).
### Tests
- Test reactions for forwarded messages
### Refactor
- `is_probably_private_reply`: Remove reaction-specific code.
- Use Rust 1.77.0 support for recursion in async functions.
### Miscellaneous Tasks
- cargo: Bump rustyline from 13.0.0 to 14.0.0.
- Update chrono from 0.4.34 to 0.4.37.
- Update from brotli 3.4.0 to brotli 4.0.0.
- Upgrade `h2` from 0.4.3 to 0.4.4.
- Upgrade `image` from 0.24.9 to 0.25.1.
- cargo: Bump fast-socks5 from 0.9.5 to 0.9.6.
## [1.137.1] - 2024-04-03
### CI
- Remove android builds for `x86` and `x86_64`.
## [1.137.0] - 2024-04-02
### API-Changes
- [**breaking**] Remove data from `DC_EVENT_INCOMING_MSG_BUNCH`.
- [**breaking**] Remove unused `dc_accounts_all_work_done()` ([#5384](https://github.com/deltachat/deltachat-core-rust/pull/5384)).
- deltachat-rpc-client: Add futures.
### Build system
- cmake: Build outside the source tree.
- nix: Add outputs for Android binaries.
- Add `repository` to Cargo.toml.
- python: Remove `setuptools_scm` dependency.
- Add development shell ([#5390](https://github.com/deltachat/deltachat-core-rust/pull/5390)).
### CI
- Update to Rust 1.77.0.
- Build deltachat-rpc-server for Android.
- Shorter names for deltachat-rpc-server jobs.
### Features / Changes
- Do not include provider hostname in `Message-ID`.
- Include 3 recent Message-IDs in `References` header.
- Include more entries into DNS fallback cache.
### Fixes
- Preserve upper-/lowercase of links parsed by `dehtml()` ([#5362](https://github.com/deltachat/deltachat-core-rust/pull/5362)).
- Rescan folders after changing `Config::SentboxWatch`.
- Do not ignore `Contact::get_by_id()` error in `from_field_to_contact_id()`.
- Put overridden sender name into message info.
- Don't send selfavatar in `SecureJoin` messages before contact verification ([#5354](https://github.com/deltachat/deltachat-core-rust/pull/5354)).
- Always set correct `chat_id` for `DC_EVENT_REACTIONS_CHANGED` ([#5419](https://github.com/deltachat/deltachat-core-rust/pull/5419)).
### Refactor
- Remove `MessageObject::from_message_id()`.
- jsonrpc: Add `msg_id` and `account_id` to `get_message()` errors.
- Cleanup `jobs` and `Params` relicts.
### Tests
- `Test_mvbox_sentbox_threads`: Check that sentbox gets configured after setting `sentbox_watch` ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
- Remove flaky time check from `test_list_from()`.
- Add failing test for #5418 (wrong `DC_EVENT_REACTIONS_CHANGED`)
### Miscellaneous Tasks
- Add `result` to .gitignore.
- cargo: Bump thiserror from 1.0.57 to 1.0.58.
- cargo: Bump tokio from 1.36.0 to 1.37.0.
- cargo: Bump pin-project from 1.1.4 to 1.1.5.
- cargo: Bump strum from 0.26.1 to 0.26.2.
- cargo: Bump uuid from 1.7.0 to 1.8.0.
- cargo: Bump toml from 0.8.10 to 0.8.12.
- cargo: Bump tokio-stream from 0.1.14 to 0.1.15.
- cargo: Bump smallvec from 1.13.1 to 1.13.2.
- cargo: Bump async-smtp from 0.9.0 to 0.9.1.
- cargo: Bump strum_macros from 0.26.1 to 0.26.2.
- cargo: Bump serde_json from 1.0.114 to 1.0.115.
- cargo: Bump anyhow from 1.0.80 to 1.0.81.
- cargo: Bump syn from 2.0.52 to 2.0.57.
- cargo: Bump futures-lite from 2.2.0 to 2.3.0.
- cargo: Bump axum from 0.7.4 to 0.7.5.
- cargo: Bump reqwest from 0.11.24 to 0.12.2.
- cargo: Bump backtrace from 0.3.69 to 0.3.71.
- cargo: Bump regex from 1.10.3 to 1.10.4.
- cargo: Update aho-corasick from 1.1.2 to 1.1.3.
- Update deny.toml.
## [1.136.6] - 2024-03-19
### Build system
- Add description to deltachat-rpc-server wheels.
- Read version from Cargo.toml in wheel-rpc-server.py.
### CI
- Update actions/cache from v3 to v4.
- Automate publishing of deltachat-rpc-server to PyPI.
### Documentation
- deltachat-rpc-server: Update deltachat-rpc-client URL.
### Miscellaneous Tasks
- Nix flake update.
## [1.136.5] - 2024-03-18
### Features / Changes
- Nicer summaries: prefer emoji over names
- Add `save_mime_headers` to debug info ([#5350](https://github.com/deltachat/deltachat-core-rust/pull/5350))
### Fixes
- Terminate ephemeral and location loop immediately on channel close.
- Update MemberListTimestamp when sending a group message.
- On iOS, use FILE (default) instead of MEMORY ([#5349](https://github.com/deltachat/deltachat-core-rust/pull/5349)).
- Add white background to recoded avatars ([#3787](https://github.com/deltachat/deltachat-core-rust/pull/3787)).
### Build system
- Add README to deltachat-rpc-client Python packages.
### Documentation
- deltachat-rpc-client: Document that 0 is a special value of `set_ephemeral_timer()`.
### Tests
- Test that reordering of Member added message results in square bracket error.
## [1.136.4] - 2024-03-11
### Build system
- nix: Make .#libdeltachat buildable on macOS.
- Build deltachat-rpc-server wheels with nix.
### CI
- Add workflow for automatic publishing of deltachat-rpc-client.
### Fixes
- Remove duplicate CHANGELOG entries for 1.135.1.
## [1.136.3] - 2024-03-09
### Features / Changes
- Start IMAP loop for sentbox only if it is configured ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
### Fixes
- Remove leading whitespace from Subject ([#5106](https://github.com/deltachat/deltachat-core-rust/pull/5106)).
- Create new Peerstate for unencrypted message with already known Autocrypt key, but a new address.
### Build system
- nix: Cleanup cross-compilation code.
- nix: Include SystemConfiguration framework on darwin systems.
### CI
- Wait for `build_windows` task before trying to publish it.
- Remove artifacts from npm package.
### Refactor
- Don't parse Autocrypt header for outgoing messages ([#5259](https://github.com/deltachat/deltachat-core-rust/pull/5259)).
- Remove `deduplicate_peerstates()`.
- Fix 2024-03-05 nightly clippy warnings.
### Miscellaneous Tasks
- deps: Bump mio from 0.8.8 to 0.8.11 in /fuzz.
- RPC client: Add missing constants ([#5110](https://github.com/deltachat/deltachat-core-rust/pull/5110)).
## [1.136.2] - 2024-03-05
### Build system
- Downgrade `cc` to 1.0.83 to fix build for Android.
### CI
- Update setup-node action.
## [1.136.1] - 2024-03-05
### Build system
- Revert to OpenSSL 3.1.
- Restore MSRV 1.70.0.
### Miscellaneous Tasks
- Update node constants.
## [1.136.0] - 2024-03-04
### Features / Changes
- Recognise Trash folder by name ([#5275](https://github.com/deltachat/deltachat-core-rust/pull/5275)).
- Send Chat-Group-Avatar as inline base64 ([#5253](https://github.com/deltachat/deltachat-core-rust/pull/5253)).
- Self-Reporting: Report number of protected/encrypted/unencrypted chats ([#5292](https://github.com/deltachat/deltachat-core-rust/pull/5292)).
### Fixes
- Don't send sync messages on self-{status,avatar} update from self-sent messages ([#5289](https://github.com/deltachat/deltachat-core-rust/pull/5289)).
- imap: Allow `maybe_network` to interrupt connection ratelimit.
- imap: Set connectivity to "connecting" only after ratelimit.
- Remove `Group-ID` from `Message-ID`.
- Prioritize protected `Message-ID` over `X-Microsoft-Original-Message-ID`.
### API-Changes
- Make `store_self_keypair` private.
- Add `ContextBuilder.build()` to build Context without opening.
- `dc_accounts_set_push_device_token` and `dc_get_push_state` APIs for iOS push notifications.
### Build system
- Tag armv6 wheels with tags accepted by PyPI.
- Unpin OpenSSL.
- Remove deprecated `unmaintained` field from deny.toml.
- Do not vendor OpenSSL when cross-compiling ([#5316](https://github.com/deltachat/deltachat-core-rust/pull/5316)).
- Increase MSRV to 1.74.0.
### CI
- Upgrade setup-python GitHub Action.
- Update to Rust 1.76 and fix clippy warnings.
- Build Python docs with Nix.
- Upload python docs without GH actions.
- Upload cffi docs without GH actions.
- Build c.delta.chat docs with nix.
### Other
- refactor: move more methods from Imap into Session.
- Add deltachat-time to sources.
### Refactor
- Remove Session from Imap structure.
- Merge ImapConfig into Imap.
- Get rid of ImapActionResult.
- Build contexts using ContextBuilder.
- Do not send `Secure-Join-Group` in `vg-request`.
### Tests
- Fix `test_verified_oneonone_chat_broken_by_device_change()` ([#5280](https://github.com/deltachat/deltachat-core-rust/pull/5280)).
- `get_protected_chat()`: Use FFIEventTracker instead of `dc_wait_next_msgs()` ([#5207](https://github.com/deltachat/deltachat-core-rust/pull/5207)).
- Fixup `tests/test_3_offline.py::TestOfflineAccountBasic::test_wrong_db`.
- Fix pytest compat ([#5317](https://github.com/deltachat/deltachat-core-rust/pull/5317)).
## [1.135.1] - 2024-02-20
### Features / Changes
@@ -310,9 +30,6 @@
- Build deltachat-repl for Windows with nix.
- Build deltachat-rpc-server with nix.
- Try to upload deltachat-rpc-server only on release.
- Fixup node-package.yml after artifact actions upgrade.
- Update to actions/checkout@v4.
- Replace download-artifact v1 with v4.
### Refactor
@@ -320,6 +37,44 @@
- Return error with a cause when failing to export keys.
- Rename incorrectly named variables in `create_keypair`.
## [1.135.1] - 2024-02-20
### CI
- Update actions/upload-artifact.
- Use actions/download-artifact@v4.
- Replace download-artifact v1 with v4.
- Update to actions/checkout@v4.
- Fixup node-package.yml after artifact actions upgrade.
### Features / Changes
- Mock SystemTime::now() for the tests.
- Remove webxdc sending limit.
- Sync self-avatar across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
- Sync Config::Selfstatus across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
### Fixes
- Context::get_info: Report displayname as "displayname" (w/o underscore).
- Never encrypt {vc,vg}-request.
### Other
- Cleanup changelog ([#5265](https://github.com/deltachat/deltachat-core-rust/pull/5265))
somehow a whole issue sneaked in :).
### Refactor
- create_keypair: Remove unnecessary map_err.
- Return error with a cause when failing to export keys.
- Rename incorrectly named variables in create_keypair.
### Tests
- Add a test on protection message sort timestamp ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
## [1.135.0] - 2024-02-13
### Features / Changes
@@ -3862,13 +3617,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.134.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.2...v1.134.0
[1.135.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.134.0...v1.135.0
[1.135.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.0...v1.135.1
[1.136.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.1...v1.136.0
[1.136.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.0...v1.136.1
[1.136.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.1...v1.136.2
[1.136.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.2...v1.136.3
[1.136.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.3...v1.136.4
[1.136.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.4...v1.136.5
[1.136.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.5...v1.136.6
[1.137.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.6...v1.137.0
[1.137.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.0...v1.137.1
[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2
+20 -10
View File
@@ -14,14 +14,24 @@ endif()
add_custom_command(
OUTPUT
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
"target/release/libdeltachat.a"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
${CARGO} build --release --no-default-features --features jsonrpc
# Build in `deltachat-ffi` directory instead of using
# `--package deltachat_ffi` to avoid feature resolver version
# "1" bug which makes `--no-default-features` affect only
# `deltachat`, but not `deltachat-ffi` package.
#
# We can't enable version "2" resolver [1] because it is not
# stable yet on rust 1.50.0.
#
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)
@@ -29,12 +39,12 @@ add_custom_target(
lib_deltachat
ALL
DEPENDS
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
"target/release/libdeltachat.a"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
Generated
+199 -181
View File
@@ -55,9 +55,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.3"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
@@ -121,9 +121,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anyhow"
version = "1.0.81"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
dependencies = [
"backtrace",
]
@@ -267,9 +267,9 @@ dependencies = [
[[package]]
name = "async-smtp"
version = "0.9.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928ea96f04e1260036cb01f82c1e5dc7c3b478e7b9463c87712297784ce3cdc6"
checksum = "8709c0d4432be428a88a06746689a9cb543e8e27ef7f61ca4d0455003a3d8c5b"
dependencies = [
"anyhow",
"base64 0.13.1",
@@ -290,7 +290,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -315,19 +315,19 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.7.5"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e"
dependencies = [
"async-trait",
"axum-core",
"base64 0.21.7",
"bytes",
"futures-util",
"http",
"http-body",
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"hyper",
"hyper 1.2.0",
"hyper-util",
"itoa",
"matchit",
@@ -341,7 +341,7 @@ dependencies = [
"serde_path_to_error",
"serde_urlencoded",
"sha1",
"sync_wrapper 1.0.0",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
"tower",
@@ -359,13 +359,13 @@ dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 0.1.2",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
@@ -373,9 +373,9 @@ dependencies = [
[[package]]
name = "backtrace"
version = "0.3.71"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
@@ -507,9 +507,9 @@ dependencies = [
[[package]]
name = "brotli"
version = "4.0.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569"
checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -518,9 +518,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "3.0.0"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -627,9 +627,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.90"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723"
[[package]]
name = "cfb-mode"
@@ -646,12 +646,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "charset"
version = "0.1.3"
@@ -664,9 +658,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.37"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -715,18 +709,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.18"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.4.18"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
dependencies = [
"anstyle",
"clap_lex",
@@ -734,9 +728,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "clipboard-win"
@@ -993,7 +987,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -1091,7 +1085,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.137.2"
version = "1.135.1"
dependencies = [
"ansi_term",
"anyhow",
@@ -1102,9 +1096,7 @@ dependencies = [
"async_zip",
"backtrace",
"base64 0.21.7",
"bitflags 1.3.2",
"brotli",
"bstr",
"chrono",
"criterion",
"deltachat-time",
@@ -1132,7 +1124,6 @@ dependencies = [
"num-traits",
"num_cpus",
"once_cell",
"openssl-src",
"parking_lot",
"percent-encoding",
"pgp",
@@ -1174,7 +1165,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.137.2"
version = "1.135.1"
dependencies = [
"anyhow",
"async-channel 2.2.0",
@@ -1198,7 +1189,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.137.2"
version = "1.135.1"
dependencies = [
"ansi_term",
"anyhow",
@@ -1213,7 +1204,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.137.2"
version = "1.135.1"
dependencies = [
"anyhow",
"deltachat",
@@ -1237,12 +1228,12 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
name = "deltachat_ffi"
version = "1.137.2"
version = "1.135.1"
dependencies = [
"anyhow",
"deltachat",
@@ -1446,7 +1437,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -1755,7 +1746,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -1768,7 +1759,7 @@ dependencies = [
"num-traits",
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -1856,9 +1847,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fast-socks5"
version = "0.9.6"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f89f36d4ee12370d30d57b16c7e190950a1a916e7dbbb5fd5a412f5ef913fe84"
checksum = "cbcc731f3c17a5053e07e6a2290918da75cd8b9b1217b419721f715674ac520c"
dependencies = [
"anyhow",
"async-trait",
@@ -2047,9 +2038,9 @@ checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
version = "2.3.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
dependencies = [
"fastrand",
"futures-core",
@@ -2066,7 +2057,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -2174,16 +2165,35 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.4"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"http 0.2.12",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 1.1.0",
"indexmap",
"slab",
"tokio",
@@ -2321,6 +2331,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http"
version = "1.1.0"
@@ -2332,6 +2353,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.0"
@@ -2339,7 +2371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http",
"http 1.1.0",
]
[[package]]
@@ -2350,8 +2382,8 @@ checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
dependencies = [
"bytes",
"futures-util",
"http",
"http-body",
"http 1.1.0",
"http-body 1.0.0",
"pin-project-lite",
]
@@ -2396,6 +2428,30 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.24",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.2.0"
@@ -2405,32 +2461,28 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2",
"http",
"http-body",
"h2 0.4.2",
"http 1.1.0",
"http-body 1.0.0",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"hyper 0.14.28",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
@@ -2440,17 +2492,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"http 1.1.0",
"http-body 1.0.0",
"hyper 1.2.0",
"pin-project-lite",
"socket2",
"tokio",
"tower",
"tower-service",
"tracing",
]
[[package]]
@@ -2513,29 +2561,17 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.1"
version = "0.24.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif",
"image-webp",
"jpeg-decoder",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a84a25dcae3ac487bc24ef280f9e20c79c9b1a3e5e32cbed3041d1c514aa87c"
dependencies = [
"byteorder",
"thiserror",
]
[[package]]
@@ -2660,6 +2696,12 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
[[package]]
name = "js-sys"
version = "0.3.69"
@@ -2986,13 +3028,12 @@ dependencies = [
[[package]]
name = "nix"
version = "0.28.0"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
"cfg_aliases",
"libc",
]
@@ -3068,7 +3109,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -3129,7 +3170,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -3170,9 +3211,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.63"
version = "0.10.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
@@ -3191,7 +3232,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -3202,18 +3243,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.1.6+3.1.4"
version = "300.2.3+3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.99"
version = "0.9.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
dependencies = [
"cc",
"libc",
@@ -3417,22 +3458,22 @@ dependencies = [
[[package]]
name = "pin-project"
version = "1.1.5"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -3932,9 +3973,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.4"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
@@ -3976,22 +4017,20 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "reqwest"
version = "0.12.2"
version = "0.11.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338"
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"h2 0.3.24",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
@@ -4004,7 +4043,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
@@ -4240,9 +4279,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustyline"
version = "14.0.0"
version = "13.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63"
checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
@@ -4257,7 +4296,7 @@ dependencies = [
"unicode-segmentation",
"unicode-width",
"utf8parse",
"windows-sys 0.52.0",
"winapi",
]
[[package]]
@@ -4435,7 +4474,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -4451,9 +4490,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.115"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
@@ -4602,9 +4641,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.13.2"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smawk"
@@ -4706,21 +4745,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.26.2"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
[[package]]
name = "strum_macros"
version = "0.26.2"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -4742,9 +4781,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.57"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2",
"quote",
@@ -4757,12 +4796,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c"
[[package]]
name = "synstructure"
version = "0.12.6"
@@ -4864,22 +4897,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.58"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.58"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -4961,9 +4994,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [
"backtrace",
"bytes",
@@ -4996,7 +5029,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -5026,9 +5059,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.15"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -5078,14 +5111,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.12"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.9",
"toml_edit 0.22.6",
]
[[package]]
@@ -5110,9 +5143,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.9"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
dependencies = [
"indexmap",
"serde",
@@ -5169,7 +5202,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -5236,7 +5269,7 @@ dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http",
"http 1.1.0",
"httparse",
"log",
"rand 0.8.5",
@@ -5373,9 +5406,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.8.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"getrandom 0.2.12",
"serde",
@@ -5463,7 +5496,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
"wasm-bindgen-shared",
]
@@ -5497,7 +5530,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -5893,7 +5926,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
"syn 2.0.52",
]
[[package]]
@@ -5913,20 +5946,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.57",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
dependencies = [
"zune-core",
"syn 2.0.52",
]
+8 -19
View File
@@ -1,10 +1,9 @@
[package]
name = "deltachat"
version = "1.137.2"
version = "1.135.1"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
repository = "https://github.com/deltachat/deltachat-core-rust"
rust-version = "1.74"
[profile.dev]
debug = 0
@@ -45,9 +44,7 @@ async-smtp = { version = "0.9", default-features = false, features = ["runtime-t
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = "0.21"
brotli = { version = "4", default-features=false, features = ["std"] }
bitflags = "1.3"
bstr = { version = "1.4.0", default-features=false, features = ["std", "alloc"] }
brotli = { version = "3.4", default-features=false, features = ["std"] }
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
@@ -55,11 +52,11 @@ escaper = "0.1"
fast-socks5 = "0.9"
fd-lock = "4"
futures = "0.3"
futures-lite = "2.3.0"
futures-lite = "2.2.0"
hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.9", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.2", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
@@ -80,7 +77,7 @@ quick-xml = "0.31"
quoted_printable = "0.5"
rand = "0.8"
regex = "1.10"
reqwest = { version = "0.12.2", features = ["json"] }
reqwest = { version = "0.11.24", features = ["json"] }
rusqlite = { version = "0.31", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.5"
@@ -96,26 +93,18 @@ textwrap = "0.16.1"
thiserror = "1"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.15", features = ["fs"] }
tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.9"
toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
# Pin OpenSSL to 3.1 releases.
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
# According to <https://www.openssl.org/policies/releasestrat.html>
# 3.1 branch will be supported until 2025-03-14.
openssl-src = "~300.1"
[dev-dependencies]
ansi_term = "0.12.0"
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = "2.3.0"
futures-lite = "2.2.0"
log = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.137.2"
version = "1.135.1"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+20 -20
View File
@@ -705,6 +705,7 @@ int dc_get_push_state (dc_context_t* context);
/**
* Standalone version of dc_accounts_all_work_done().
* Only used by the python tests.
*/
int dc_all_work_done (dc_context_t* context);
@@ -3098,6 +3099,23 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
/**
* This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
*
* iOS can:
* - call dc_start_io() (in case IO was not running)
* - call dc_maybe_network()
* - while dc_accounts_all_work_done() returns false:
* - Wait for #DC_EVENT_CONNECTIVITY_CHANGED
*
* @memberof dc_accounts_t
* @param accounts The account manager as created by dc_accounts_new().
* @return Whether all accounts finished their background work.
* #DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
*/
int dc_accounts_all_work_done (dc_accounts_t* accounts);
/**
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
* If IO is already running, nothing happens.
@@ -6056,12 +6074,10 @@ void dc_event_unref(dc_event_t* event);
* Downloading a bunch of messages just finished. This is an
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
* UI may store #DC_EVENT_INCOMING_MSG events
* and display notifications for all messages at once
* when this event arrives.
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
*
* @param data1 0
* @param data2 0
* @param data2 (char*) msg_ids, a json object with the message ids.
*/
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
@@ -7296,22 +7312,6 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the provider's domain.
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
/// "You reacted %1$s to '%2$s'"
///
/// `%1$s` will be replaced by the reaction, usually an emoji
/// `%2$s` will be replaced by the summary of the message the reaction refers to
///
/// Used in summaries.
#define DC_STR_YOU_REACTED 176
/// "%1$s reacted %2$s to '%3$s'"
///
/// `%1$s` will be replaced by the name the contact who reacted
/// `%2$s` will be replaced by the reaction, usually an emoji
/// `%3$s` will be replaced by the summary of the message the reaction refers to
///
/// Used in summaries.
#define DC_STR_REACTED_BY 177
/**
* @}
+16 -2
View File
@@ -719,8 +719,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::WebxdcStatusUpdate { .. }
| EventType::WebxdcInstanceDeleted { .. }
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. }
| EventType::IncomingMsgBunch { .. } => ptr::null_mut(),
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
@@ -732,6 +731,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
.unwrap_or_default()
.to_c_string()
.unwrap_or_default()
.into_raw(),
EventType::ConfigSynced { key } => {
let data2 = key.to_string().to_c_string().unwrap_or_default();
data2.into_raw()
@@ -4851,6 +4855,16 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_all_work_done()");
return 0;
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.all_work_done().await as libc::c_int })
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
if accounts.is_null() {
+3 -4
View File
@@ -1,11 +1,10 @@
[package]
name = "deltachat-jsonrpc"
version = "1.137.2"
version = "1.135.1"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
license = "MPL-2.0"
repository = "https://github.com/deltachat/deltachat-core-rust"
[[bin]]
name = "deltachat-jsonrpc-server"
@@ -25,7 +24,7 @@ futures = { version = "0.3.30" }
serde_json = "1"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.37.0" }
tokio = { version = "1.33.0" }
sanitize-filename = "0.5"
walkdir = "2.5.0"
base64 = "0.21"
@@ -35,7 +34,7 @@ axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.33.0", features = ["full", "rt-multi-thread"] }
[features]
+8 -9
View File
@@ -1097,12 +1097,9 @@ impl CommandApi {
.collect::<Vec<JSONRPCMessageListItem>>())
}
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
MessageObject::from_message_id(&ctx, message_id).await
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
@@ -1122,7 +1119,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
for message_id in message_ids {
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
messages.insert(
message_id,
match message_result {
@@ -2045,9 +2042,11 @@ impl CommandApi {
)
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
Ok((msg_id.to_u32(), message))
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await?
.to_u32();
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
Ok((msg_id, message))
}
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
+8 -4
View File
@@ -101,15 +101,17 @@ pub enum EventType {
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
#[serde(rename_all = "camelCase")]
IncomingMsg { chat_id: u32, msg_id: u32 },
/// Downloading a bunch of messages just finished. This is an
/// Downloading a bunch of messages just finished. This is an experimental
/// event to allow the UI to only show one notification per message bunch,
/// instead of cluttering the user with many notifications.
///
/// msg_ids contains the message ids.
#[serde(rename_all = "camelCase")]
IncomingMsgBunch,
IncomingMsgBunch { msg_ids: Vec<u32> },
/// Messages were seen or noticed.
/// chat id is always set.
@@ -285,7 +287,9 @@ impl From<CoreEventType> for EventType {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::IncomingMsgBunch => IncomingMsgBunch,
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},
@@ -105,6 +105,11 @@ enum MessageQuote {
}
impl MessageObject {
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
let msg_id = MsgId::new(message_id);
Self::from_msg_id(context, msg_id).await
}
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
+1 -1
View File
@@ -53,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.137.2"
"version": "1.135.1"
}
+2 -3
View File
@@ -1,9 +1,8 @@
[package]
name = "deltachat-repl"
version = "1.137.2"
version = "1.135.1"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies]
ansi_term = "0.12.1"
@@ -13,7 +12,7 @@ dirs = "5"
log = "0.4.21"
pretty_env_logger = "0.5"
rusqlite = "0.31"
rustyline = "14"
rustyline = "13"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features]
+9 -11
View File
@@ -3,7 +3,7 @@ extern crate dirs;
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Result};
use deltachat::chat::{
@@ -33,6 +33,14 @@ use tokio::fs;
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({bits})...");
if 0 != bits & 1 {
context
.sql()
.execute("DELETE FROM jobs;", ())
.await
.unwrap();
println!("(1) Jobs reset.");
}
if 0 != bits & 2 {
context
.sql()
@@ -339,8 +347,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
export-keys\n\
import-keys\n\
export-setup\n\
dump <filename>\n\n
read <filename>\n\n
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
reset <flags>\n\
stop\n\
@@ -516,14 +522,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
&setup_code,
);
}
"dump" => {
ensure!(!arg1.is_empty(), "Argument <filename> missing.");
serialize_database(&context, arg1).await?;
}
"read" => {
ensure!(!arg1.is_empty(), "Argument <filename> missing.");
deserialize_database(&context, arg1).await?;
}
"poke" => {
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}
+7 -3
View File
@@ -1,10 +1,9 @@
[build-system]
requires = ["setuptools>=45"]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.137.2"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -20,7 +19,9 @@ classifiers = [
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]
readme = "README.md"
dynamic = [
"version"
]
[tool.setuptools.package-data]
deltachat_rpc_client = [
@@ -30,6 +31,9 @@ deltachat_rpc_client = [
[project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
[tool.setuptools_scm]
root = ".."
[tool.black]
line-length = 120
@@ -168,33 +168,3 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
return "removed", addr, addr
return None
class futuremethod: # noqa: N801
"""Decorator for async methods."""
def __init__(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)
def f():
try:
generator.send(res())
except StopIteration as e:
return e.value
return f
def wrapper(*args):
f = future(*args)
return f()
wrapper.future = future
return wrapper
@@ -2,7 +2,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from warnings import warn
from ._utils import AttrDict, futuremethod
from ._utils import AttrDict
from .chat import Chat
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
from .contact import Contact
@@ -76,16 +76,9 @@ class Account:
"""Get self avatar."""
return self.get_config("selfavatar")
def check_qr(self, qr):
return self._rpc.check_qr(self.id, qr)
def set_config_from_qr(self, qr: str):
self._rpc.set_config_from_qr(self.id, qr)
@futuremethod
def configure(self):
def configure(self) -> None:
"""Configure an account."""
yield self._rpc.configure.future(self.id)
self._rpc.configure(self.id)
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
@@ -84,9 +84,7 @@ class Chat:
self._rpc.set_chat_name(self.account.id, self.id, name)
def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat in seconds.
0 means the timer is disabled, use 1 for immediate deletion."""
"""Set ephemeral timer of this chat."""
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:
@@ -61,15 +61,6 @@ class EventType(str, Enum):
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
class ChatId(IntEnum):
"""Special chat ids"""
TRASH = 3
ARCHIVED_LINK = 6
ALLDONE_HINT = 7
LAST_SPECIAL = 9
class ChatType(IntEnum):
"""Chat types"""
@@ -131,107 +122,3 @@ class SystemMessageType(str, Enum):
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
MULTI_DEVICE_SYNC = "MultiDeviceSync"
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
class MessageState(IntEnum):
"""State of the message."""
UNDEFINED = 0
IN_FRESH = 10
IN_NOTICED = 13
IN_SEEN = 16
OUT_PREPARING = 18
OUT_DRAFT = 19
OUT_PENDING = 20
OUT_FAILED = 24
OUT_DELIVERED = 26
OUT_MDN_RCVD = 28
class MessageId(IntEnum):
"""Special message ids"""
DAYMARKER = 9
LAST_SPECIAL = 9
class CertificateChecks(IntEnum):
"""Certificate checks mode"""
AUTOMATIC = 0
STRICT = 1
ACCEPT_INVALID_CERTIFICATES = 3
class Connectivity(IntEnum):
"""Connectivity states"""
NOT_CONNECTED = 1000
CONNECTING = 2000
WORKING = 3000
CONNECTED = 4000
class KeyGenType(IntEnum):
"""Type of the key to generate"""
DEFAULT = 0
RSA2048 = 1
ED25519 = 2
RSA4096 = 3
# "Lp" means "login parameters"
class LpAuthFlag(IntEnum):
"""Authorization flags"""
OAUTH2 = 0x2
NORMAL = 0x4
class MediaQuality(IntEnum):
"""Media quality setting"""
BALANCED = 0
WORSE = 1
class ProviderStatus(IntEnum):
"""Provider status according to manual testing"""
OK = 1
PREPARATION = 2
BROKEN = 3
class PushNotifyState(IntEnum):
"""Push notifications state"""
NOT_CONNECTED = 0
HEARTBEAT = 1
CONNECTED = 2
class ShowEmails(IntEnum):
"""Show emails mode"""
OFF = 0
ACCEPTED_CONTACTS = 1
ALL = 2
class SocketSecurity(IntEnum):
"""Socket security"""
AUTOMATIC = 0
SSL = 1
STARTTLS = 2
PLAIN = 3
class VideochatType(IntEnum):
"""Video chat URL type"""
UNKNOWN = 0
BASICWEBRTC = 1
JITSI = 2
@@ -5,7 +5,6 @@ from typing import AsyncGenerator, List, Optional
import pytest
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from ._utils import futuremethod
from .rpc import Rpc
@@ -38,10 +37,9 @@ class ACFactory:
assert not account.is_configured()
return account
@futuremethod
def new_configured_account(self):
def new_configured_account(self) -> Account:
account = self.new_preconfigured_account()
yield account.configure.future()
account.configure()
assert account.is_configured()
return account
@@ -51,9 +49,8 @@ class ACFactory:
bot.configure(credentials["email"], credentials["password"])
return bot
@futuremethod
def get_online_account(self):
account = yield self.new_configured_account.future()
def get_online_account(self) -> Account:
account = self.new_configured_account()
account.start_io()
while True:
event = account.wait_for_event()
@@ -62,8 +59,7 @@ class ACFactory:
return account
def get_online_accounts(self, num: int) -> List[Account]:
futures = [self.get_online_account.future() for _ in range(num)]
return [f() for f in futures]
return [self.get_online_account() for _ in range(num)]
def resetup_account(self, ac: Account) -> Account:
"""Resetup account from scratch, losing the encryption key."""
@@ -13,48 +13,6 @@ class JsonRpcError(Exception):
pass
class RpcFuture:
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
self.rpc = rpc
self.request_id = request_id
self.event = event
def __call__(self):
self.event.wait()
response = self.rpc.request_results.pop(self.request_id)
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return None
class RpcMethod:
def __init__(self, rpc: "Rpc", name: str):
self.rpc = rpc
self.name = name
def __call__(self, *args) -> Any:
"""Synchronously calls JSON-RPC method."""
future = self.future(*args)
return future()
def future(self, *args) -> Any:
"""Asynchronously calls JSON-RPC method."""
request_id = next(self.rpc.id_iterator)
request = {
"jsonrpc": "2.0",
"method": self.name,
"params": args,
"id": request_id,
}
event = Event()
self.rpc.request_events[request_id] = event
self.rpc.request_queue.put(request)
return RpcFuture(self.rpc, request_id, event)
class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to subprocess.Popen()"""
@@ -187,4 +145,24 @@ class Rpc:
return queue.get()
def __getattr__(self, attr: str):
return RpcMethod(self, attr)
def method(*args) -> Any:
request_id = next(self.id_iterator)
request = {
"jsonrpc": "2.0",
"method": attr,
"params": args,
"id": request_id,
}
event = Event()
self.request_events[request_id] = event
self.request_queue.put(request)
event.wait()
response = self.request_results.pop(request_id)
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return None
return method
+1 -38
View File
@@ -1,7 +1,7 @@
import logging
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
from deltachat_rpc_client import Chat, SpecialContactId
def test_qr_setup_contact(acfactory, tmp_path) -> None:
@@ -579,40 +579,3 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 is still "not verified" for ac2 due to inconsistent state.
assert not ac2_contact_ac1.get_snapshot().is_verified
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
logging.info("Alice withdraws QR code.")
qr = alice.check_qr(qr_code)
assert qr["kind"] == "withdrawVerifyGroup"
alice.set_config_from_qr(qr_code)
logging.info("Bob scans withdrawn QR code.")
bob_chat = bob.secure_join(qr_code)
logging.info("Bob scanned withdrawn QR code")
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
break
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.137.2"
version = "1.135.1"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,11 +15,11 @@ deltachat = { path = "..", default-features = false }
anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "2.3.0"
futures-lite = "2.2.0"
log = "0.4"
serde_json = "1"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.37.0", features = ["io-std"] }
tokio = { version = "1.33.0", features = ["io-std"] }
tokio-util = "0.7.9"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
+1 -1
View File
@@ -30,7 +30,7 @@ deltachat-rpc-server
The common use case for this program is to create bindings to use Delta Chat core from programming
languages other than Rust, for example:
1. Python: https://pypi.org/project/deltachat-rpc-client/
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
Run `deltachat-rpc-server --version` to check the version of the server.
+4 -1
View File
@@ -39,6 +39,10 @@ skip = [
{ name = "ed25519", version = "1.5.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "getrandom", version = "<0.2" },
{ name = "h2", version = "0.3.22" },
{ name = "http-body", version = "0.4.5" },
{ name = "http", version = "0.2.11" },
{ name = "hyper", version = "0.14.27" },
{ name = "idna", version = "0.4.0" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
@@ -55,7 +59,6 @@ skip = [
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "toml_edit", version = "0.21.1" },
Generated
+28 -153
View File
@@ -1,58 +1,16 @@
{
"nodes": {
"android": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1710633978,
"narHash": "sha256-yemnwSvW7cdWtXGpivFA5jDO35rGPs6fqxlQ4l6ODXs=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "e91fb3d8517538e5ad9b422c9a4f604b56008a9e",
"type": "github"
},
"original": {
"owner": "tadfisher",
"repo": "android-nixpkgs",
"type": "github"
}
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"android",
"nixpkgs"
]
},
"locked": {
"lastModified": 1708939976,
"narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=",
"owner": "numtide",
"repo": "devshell",
"rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"nixpkgs": "nixpkgs",
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1710742993,
"narHash": "sha256-W0PQCe0bW3hKF5lHawXrKynBcdSP18Qa4sb8DcUfOqI=",
"lastModified": 1707891749,
"narHash": "sha256-SeikNYElHgv8uVMbiA9/pU3Cce7ssIsiM8CnEiwd1Nc=",
"owner": "nix-community",
"repo": "fenix",
"rev": "6f2fec850f569d61562d3a47dc263f19e9c7d825",
"rev": "3115aab064ef38cccd792c45429af8df43d6d277",
"type": "github"
},
"original": {
@@ -66,47 +24,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
@@ -117,7 +39,7 @@
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs_3"
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1698420672,
@@ -135,11 +57,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"lastModified": 1705332318,
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
"type": "github"
},
"original": {
@@ -150,15 +72,15 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"owner": "NixOS",
"lastModified": 1707689078,
"narHash": "sha256-UUGmRa84ZJHpGZ1WZEBEUOzaPOWG8LZ0yPg1pdDF/yM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"rev": "f9d39fb9aff0efee4a3d5f4a6d7c17701d38a1d8",
"type": "github"
},
"original": {
"owner": "NixOS",
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
@@ -166,27 +88,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1710631334,
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1710765496,
"narHash": "sha256-p7ryWEeQfMwTB6E0wIUd5V2cFTgq+DRRBz2hYGnJZyA=",
"lastModified": 1707743206,
"narHash": "sha256-AehgH64b28yKobC/DAWYZWkJBxL/vP83vkY+ag2Hhy4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e367f7a1fb93137af22a3908f00b9a35e2d286a7",
"rev": "2d627a2a704708673e56346fcb13d25344b8eaf3",
"type": "github"
},
"original": {
@@ -194,13 +100,13 @@
"type": "indirect"
}
},
"nixpkgs_4": {
"nixpkgs_3": {
"locked": {
"lastModified": 1710631334,
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
"lastModified": 1707689078,
"narHash": "sha256-UUGmRa84ZJHpGZ1WZEBEUOzaPOWG8LZ0yPg1pdDF/yM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
"rev": "f9d39fb9aff0efee4a3d5f4a6d7c17701d38a1d8",
"type": "github"
},
"original": {
@@ -212,22 +118,21 @@
},
"root": {
"inputs": {
"android": "android",
"fenix": "fenix",
"flake-utils": "flake-utils_3",
"flake-utils": "flake-utils",
"naersk": "naersk",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_4"
"nixpkgs": "nixpkgs_3"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1710708100,
"narHash": "sha256-Jd6pmXlwKk5uYcjyO/8BfbUVmx8g31Qfk7auI2IG66A=",
"lastModified": 1707849817,
"narHash": "sha256-If6T0MDErp3/z7DBlpG4bV46IPP+7BWSlgTI88cmbw0=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "b6d1887bc4f9543b6c6bf098179a62446f34a6c3",
"rev": "a02a219773629686bd8ff123ca1aa995fa50d976",
"type": "github"
},
"original": {
@@ -251,36 +156,6 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+142 -351
View File
@@ -6,84 +6,14 @@
naersk.url = "github:nix-community/naersk";
nix-filter.url = "github:numtide/nix-filter";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
android.url = "github:tadfisher/android-nixpkgs";
};
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs.stdenv) isDarwin;
fenixPkgs = fenix.packages.${system};
naersk' = pkgs.callPackage naersk { };
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
});
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
rustSrc = nix-filter.lib {
root = ./.;
# Include only necessary files
# to avoid rebuilds e.g. when README.md or flake.nix changes.
include = [
./benches
./assets
./Cargo.lock
./Cargo.toml
./CMakeLists.txt
./CONTRIBUTING.md
./deltachat_derive
./deltachat-ffi
./deltachat-jsonrpc
./deltachat-ratelimit
./deltachat-repl
./deltachat-rpc-client
./deltachat-time
./deltachat-rpc-server
./format-flowed
./release-date.in
./src
];
exclude = [
(nix-filter.lib.matchExt "nix")
"flake.lock"
];
};
# Map from architecture name to rust targets and nixpkgs targets.
arch2targets = {
"x86_64-linux" = {
rustTarget = "x86_64-unknown-linux-musl";
crossTarget = "x86_64-unknown-linux-musl";
};
"armv7l-linux" = {
rustTarget = "armv7-unknown-linux-musleabihf";
crossTarget = "armv7l-unknown-linux-musleabihf";
};
"armv6l-linux" = {
rustTarget = "arm-unknown-linux-musleabihf";
crossTarget = "armv6l-unknown-linux-musleabihf";
};
"aarch64-linux" = {
rustTarget = "aarch64-unknown-linux-musl";
crossTarget = "aarch64-unknown-linux-musl";
};
"i686-linux" = {
rustTarget = "i686-unknown-linux-musl";
crossTarget = "i686-unknown-linux-musl";
};
"x86_64-darwin" = {
rustTarget = "x86_64-apple-darwin";
crossTarget = "x86_64-darwin";
};
"aarch64-darwin" = {
rustTarget = "aarch64-apple-darwin";
crossTarget = "aarch64-darwin";
};
};
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
@@ -101,9 +31,6 @@
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
};
@@ -111,6 +38,8 @@
mkWin64RustPackage = packageName:
let
rustTarget = "x86_64-pc-windows-gnu";
in
let
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
@@ -177,12 +106,13 @@
model = "win32";
package = null;
};
})).overrideAttrs (oldAttr: {
})).overrideAttrs (oldAttr: rec{
configureFlags = oldAttr.configureFlags ++ [
"--disable-sjlj-exceptions --with-dwarf2"
];
})
);
winStdenv = pkgsWin32.buildPackages.overrideCC pkgsWin32.stdenv winCC;
in
naerskWin.buildPackage rec {
pname = packageName;
@@ -211,10 +141,8 @@
LD = "${winCC}/bin/${winCC.targetPrefix}cc";
};
mkCrossRustPackage = arch: packageName:
mkCrossRustPackage = rustTarget: crossTarget: packageName:
let
rustTarget = arch2targets."${arch}".rustTarget;
crossTarget = arch2targets."${arch}".crossTarget;
pkgsCross = import nixpkgs {
system = system;
crossSystem.config = crossTarget;
@@ -236,10 +164,7 @@
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
src = pkgs.lib.cleanSource ./.;
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
@@ -252,290 +177,156 @@
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
OPENSSL_LIB_DIR = "${pkgsCross.pkgsStatic.openssl.out}/lib";
OPENSSL_INCLUDE_DIR = "${pkgsCross.pkgsStatic.openssl.dev}/include";
OPENSSL_STATIC = "1";
OPENSSL_NO_VENDOR = "1";
};
androidAttrs = {
armeabi-v7a = {
cc = "armv7a-linux-androideabi19-clang";
rustTarget = "armv7-linux-androideabi";
};
arm64-v8a = {
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
};
mkAndroidRustPackage = arch: packageName:
let
rustTarget = androidAttrs.${arch}.rustTarget;
toolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
targetCcName = androidAttrs.${arch}.cc;
targetCc = "${targetToolchain}/bin/${targetCcName}";
in
naersk-lib.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${targetCc}";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${targetCc}";
LD = "${targetCc}";
};
mkAndroidPackages = arch: {
"deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server";
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
};
mkRustPackages = arch:
let
rpc-server = mkCrossRustPackage arch "deltachat-rpc-server";
in
{
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
"deltachat-rpc-server-${arch}" = rpc-server;
"deltachat-rpc-server-${arch}-wheel" =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
};
mk-aarch64-RustPackage = mkCrossRustPackage "aarch64-unknown-linux-musl" "aarch64-unknown-linux-musl";
mk-i686-RustPackage = mkCrossRustPackage "i686-unknown-linux-musl" "i686-unknown-linux-musl";
mk-x86_64-RustPackage = mkCrossRustPackage "x86_64-unknown-linux-musl" "x86_64-unknown-linux-musl";
mk-armv7l-RustPackage = mkCrossRustPackage "armv7-unknown-linux-musleabihf" "armv7l-unknown-linux-musleabihf";
mk-armv6l-RustPackage = mkCrossRustPackage "arm-unknown-linux-musleabihf" "armv6l-unknown-linux-musleabihf";
in
{
formatter = pkgs.nixpkgs-fmt;
packages =
mkRustPackages "aarch64-linux" //
mkRustPackages "i686-linux" //
mkRustPackages "x86_64-linux" //
mkRustPackages "armv7l-linux" //
mkRustPackages "armv6l-linux" //
mkAndroidPackages "armeabi-v7a" //
mkAndroidPackages "arm64-v8a" //
mkAndroidPackages "x86" //
mkAndroidPackages "x86_64" // rec {
# Run with `nix run .#deltachat-repl foo.db`.
deltachat-repl = mkRustPackage "deltachat-repl";
deltachat-rpc-server = mkRustPackage "deltachat-rpc-server";
packages = rec {
# Run with `nix run .#deltachat-repl foo.db`.
deltachat-repl = mkRustPackage "deltachat-repl";
deltachat-rpc-server = mkRustPackage "deltachat-rpc-server";
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win64-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win64-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win64
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win32-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win32-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win32
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
# Run `nix build .#docs` to get C docs generated in `./result/`.
docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
libdeltachat =
pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
deltachat-repl-aarch64-linux = mk-aarch64-RustPackage "deltachat-repl";
deltachat-rpc-server-aarch64-linux = mk-aarch64-RustPackage "deltachat-rpc-server";
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
deltachat-repl-i686-linux = mk-i686-RustPackage "deltachat-repl";
deltachat-rpc-server-i686-linux = mk-i686-RustPackage "deltachat-rpc-server";
deltachat-repl-x86_64-linux = mk-x86_64-RustPackage "deltachat-repl";
deltachat-rpc-server-x86_64-linux = mk-x86_64-RustPackage "deltachat-rpc-server";
deltachat-repl-armv7l-linux = mk-armv7l-RustPackage "deltachat-repl";
deltachat-rpc-server-armv7l-linux = mk-armv7l-RustPackage "deltachat-rpc-server";
deltachat-repl-armv6l-linux = mk-armv6l-RustPackage "deltachat-repl";
deltachat-rpc-server-armv6l-linux = mk-armv6l-RustPackage "deltachat-rpc-server";
# Run `nix build .#docs` to get C docs generated in `./result/`.
docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
libdeltachat =
pkgs.stdenv.mkDerivation rec {
pname = "libdeltachat";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
# Include only necessary files
# to avoid rebuilds e.g. when README.md or flake.nix changes.
include = [
./benches
./assets
./Cargo.lock
./Cargo.toml
./CMakeLists.txt
./CONTRIBUTING.md
./deltachat_derive
./deltachat-ffi
./deltachat-jsonrpc
./deltachat-ratelimit
./deltachat-repl
./deltachat-rpc-client
./deltachat-time
./deltachat-rpc-server
./format-flowed
./release-date.in
./src
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
# Source package for deltachat-rpc-server.
# Fake package that downloads Linux version,
# needed to install deltachat-rpc-server on Android with `pip`.
deltachat-rpc-server-source =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-source";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-client =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
exclude = [
(nix-filter.lib.matchExt "nix")
"flake.lock"
];
};
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
deltachat-python =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-python";
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
libdeltachat
];
nativeBuildInputs = [
pkgs.pkg-config
];
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.pkgconfig
pkgs.python3Packages.cffi
pkgs.python3Packages.imap-tools
pkgs.python3Packages.pluggy
pkgs.python3Packages.requests
];
};
python-docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
};
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
(fenixPkgs.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
fenixPkgs.rust-analyzer
perl # needed to build vendored OpenSSL
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
deltachat-rpc-client =
pkgs.python3Packages.buildPythonPackage rec {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.setuptools_scm
];
};
deltachat-python =
pkgs.python3Packages.buildPythonPackage rec {
pname = "deltachat-python";
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
libdeltachat
];
nativeBuildInputs = [
pkgs.pkg-config
];
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.setuptools_scm
pkgs.python3Packages.pkgconfig
pkgs.python3Packages.cffi
pkgs.python3Packages.imap-tools
pkgs.python3Packages.pluggy
pkgs.python3Packages.requests
];
};
python-docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
};
}
);
+165 -358
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -115,9 +115,6 @@ module.exports = {
DC_PROVIDER_STATUS_BROKEN: 3,
DC_PROVIDER_STATUS_OK: 1,
DC_PROVIDER_STATUS_PREPARATION: 2,
DC_PUSH_CONNECTED: 2,
DC_PUSH_HEARTBEAT: 1,
DC_PUSH_NOT_CONNECTED: 0,
DC_QR_ACCOUNT: 250,
DC_QR_ADDR: 320,
DC_QR_ASK_VERIFYCONTACT: 200,
@@ -257,7 +254,6 @@ module.exports = {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_REACTED_BY: 177,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
@@ -285,7 +281,6 @@ module.exports = {
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
DC_STR_VOICEMESSAGE: 7,
DC_STR_WELCOME_MESSAGE: 71,
DC_STR_YOU_REACTED: 176,
DC_TEXT1_DRAFT: 1,
DC_TEXT1_SELF: 3,
DC_TEXT1_USERNAME: 2,
-5
View File
@@ -115,9 +115,6 @@ export enum C {
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_PUSH_CONNECTED = 2,
DC_PUSH_HEARTBEAT = 1,
DC_PUSH_NOT_CONNECTED = 0,
DC_QR_ACCOUNT = 250,
DC_QR_ADDR = 320,
DC_QR_ASK_VERIFYCONTACT = 200,
@@ -257,7 +254,6 @@ export enum C {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_REACTED_BY = 177,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
@@ -285,7 +281,6 @@ export enum C {
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_STR_YOU_REACTED = 176,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
+9
View File
@@ -3048,6 +3048,14 @@ NAPI_METHOD(dcn_accounts_select_account) {
NAPI_RETURN_UINT32(result);
}
NAPI_METHOD(dcn_accounts_all_work_done) {
NAPI_ARGV(1);
NAPI_DCN_ACCOUNTS();
int result = dc_accounts_all_work_done(dcn_accounts->dc_accounts);
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_accounts_start_io) {
NAPI_ARGV(1);
NAPI_DCN_ACCOUNTS();
@@ -3374,6 +3382,7 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_accounts_get_account);
NAPI_EXPORT_FUNCTION(dcn_accounts_get_selected_account);
NAPI_EXPORT_FUNCTION(dcn_accounts_select_account);
NAPI_EXPORT_FUNCTION(dcn_accounts_all_work_done);
NAPI_EXPORT_FUNCTION(dcn_accounts_start_io);
NAPI_EXPORT_FUNCTION(dcn_accounts_stop_io);
NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network);
+1 -1
View File
@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.137.2"
"version": "1.135.1"
}
+9 -2
View File
@@ -1,10 +1,9 @@
[build-system]
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2", "cffi>=1.0.0", "pkgconfig"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.137.2"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"
@@ -27,6 +26,9 @@ dependencies = [
"pluggy",
"requests",
]
dynamic = [
"version"
]
[project.urls]
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
@@ -42,6 +44,11 @@ deltachat = [
"py.typed"
]
[tool.setuptools_scm]
root = ".."
tag_regex = '^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
git_describe_command = "git describe --dirty --tags --long --match v*.*"
[tool.black]
line-length = 120
+2 -9
View File
@@ -547,14 +547,13 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac2_addr
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
"""Another test for the bug #3836:
- Bob has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
- Bob joins the group.
- Bob's second devices goes online, but sees a contact request instead of the verified group.
- The "member added" message is not a system message but a plain text message.
- Bob's second device doesn't display the Alice's avatar (bug #5354).
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
missing, cannot encrypt".
"""
@@ -569,10 +568,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
ac2_offl.import_self_keys(str(dir))
ac2_offl.stop_io()
lp.sec("ac1: set avatar")
avatar_path = data.get_path("d.png")
ac1.set_avatar(avatar_path)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
@@ -585,13 +580,11 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
ac2_offl.start_io()
# Receive "Member Me (<addr>) added by <addr>." message.
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
contact = msg_in.get_sender_contact()
assert msg_in.is_system_message()
assert contact.addr == ac1.get_config("addr")
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message")
msg_out = chat2.send_text("hello")
+1 -9
View File
@@ -399,7 +399,7 @@ def test_enable_mvbox_move(acfactory, lp):
def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
lp.sec("ac2: start without mvbox/sentbox threads")
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
@@ -407,18 +407,10 @@ def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac2 and ac1: waiting for configuration")
acfactory.bring_accounts_online()
lp.sec("ac1: create and configure sentbox")
ac1.direct_imap.create_folder("Sent")
ac1.set_config("sentbox_watch", "1")
lp.sec("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
while ac1.get_config("configured_sentbox_folder") != "Sent":
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def test_move_works(acfactory):
ac1 = acfactory.new_online_configuring_account()
+1 -1
View File
@@ -1 +1 @@
2024-04-05
2024-02-20
+1 -1
View File
@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.77.1
RUST_VERSION=1.76.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
-2
View File
@@ -73,8 +73,6 @@ def main():
"deltachat-jsonrpc/Cargo.toml",
"deltachat-rpc-server/Cargo.toml",
"deltachat-repl/Cargo.toml",
"python/pyproject.toml",
"deltachat-rpc-client/pyproject.toml",
]
try:
opts = parser.parse_args()
+58 -45
View File
@@ -4,23 +4,20 @@ from pathlib import Path
from wheel.wheelfile import WheelFile
import tomllib
import tarfile
import sys
from io import BytesIO
def metadata_contents(version):
readme_text = (Path("deltachat-rpc-server") / "README.md").read_text()
return f"""Metadata-Version: 2.1
Name: deltachat-rpc-server
Version: {version}
Summary: Delta Chat JSON-RPC server
Description-Content-Type: text/markdown
{readme_text}
"""
def build_source_package(version, filename):
def build_source_package(version):
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
with tarfile.open(filename, "w:gz") as pkg:
def pack(name, contents):
@@ -101,7 +98,7 @@ setup(
def build_wheel(version, binary, tag, windows=False):
filename = f"deltachat_rpc_server-{version}-{tag}.whl"
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
with WheelFile(filename, "w") as wheel:
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
@@ -128,11 +125,9 @@ def main():
Path(binary).chmod(0o755)
wheel.write(
binary,
(
"deltachat_rpc_server/deltachat-rpc-server.exe"
if windows
else "deltachat_rpc_server/deltachat-rpc-server"
),
"deltachat_rpc_server/deltachat-rpc-server.exe"
if windows
else "deltachat_rpc_server/deltachat-rpc-server",
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/METADATA",
@@ -148,41 +143,59 @@ def main():
)
arch2tags = {
"x86_64-linux": "manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
"armv7l-linux": "linux_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
"armv6l-linux": "linux_armv6l",
"aarch64-linux": "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
"i686-linux": "manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
"win64": "win_amd64",
"win32": "win32",
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
"x86_64-darwin": "macosx_10_7_x86_64",
"aarch64-darwin": "macosx_11_0_arm64",
}
def main():
with Path("Cargo.toml").open("rb") as fp:
cargo_manifest = tomllib.load(fp)
version = cargo_manifest["package"]["version"]
if sys.argv[1] == "source":
filename = f"deltachat-rpc-server-{version}.tar.gz"
build_source_package(version, filename)
else:
arch = sys.argv[1]
executable = sys.argv[2]
tags = arch2tags[arch]
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
cargo_toml = tomllib.load(f)
version = cargo_toml["package"]["version"]
Path("dist").mkdir(exist_ok=True)
build_source_package(version)
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-linux",
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-armv7l-linux",
"py3-none-linux_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
)
build_wheel(
version,
"dist/deltachat-rpc-server-armv6l-linux",
"py3-none-linux_armv6l",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-linux",
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-i686-linux",
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
)
if arch in ["win32", "win64"]:
build_wheel(
version,
executable,
f"py3-none-{tags}",
windows=True,
)
else:
build_wheel(version, executable, f"py3-none-{tags}")
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-macos",
"py3-none-macosx_10_7_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-macos",
"py3-none-macosx_11_0_arm64",
)
build_wheel(
version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
)
build_wheel(
version,
"dist/deltachat-rpc-server-win64.exe",
"py3-none-win_amd64",
windows=True,
)
main()
+2 -3
View File
@@ -119,9 +119,8 @@ All group members form the member list.
To allow different groups with the same members,
groups are identified by a group-id.
The group-id MUST be created only from the characters
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`,
MUST have a length of at least 11 characters
and no more than 32 characters.
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`
and MUST have a length of at least 11 characters.
Groups MUST have a group-name.
The group-name is any non-zero-length UTF-8 string.
+19
View File
@@ -253,6 +253,25 @@ impl Accounts {
self.accounts.keys().copied().collect()
}
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
///
/// Returns whether all accounts finished their background work.
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
///
/// iOS can:
/// - call dc_start_io() (in case IO was not running)
/// - call dc_maybe_network()
/// - while dc_accounts_all_work_done() returns false:
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
pub async fn all_work_done(&self) -> bool {
for account in self.accounts.values() {
if !account.all_work_done().await {
return false;
}
}
true
}
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
pub async fn start_io(&mut self) {
for account in self.accounts.values_mut() {
+11 -87
View File
@@ -5,14 +5,12 @@ use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use std::iter::FusedIterator;
use std::mem;
use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::codecs::jpeg::JpegEncoder;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
@@ -36,12 +34,6 @@ pub struct BlobObject<'a> {
name: String,
}
#[derive(Debug, Clone)]
enum ImageOutputFormat {
Png,
Jpeg { quality: u8 },
}
impl<'a> BlobObject<'a> {
/// Creates a new blob object with a unique name.
///
@@ -421,8 +413,6 @@ impl<'a> BlobObject<'a> {
max_bytes: usize,
strict_limits: bool,
) -> Result<Option<String>> {
// Add white background only to avatars to spare the CPU.
let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let res = tokio::task::block_in_place(move || {
@@ -456,19 +446,13 @@ impl<'a> BlobObject<'a> {
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let jpeg_quality = 75;
let fmt = ImageFormat::from_path(&blob_abs);
let ofmt = match fmt {
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
Ok(ImageFormat::Jpeg) => {
add_white_bg = false;
ImageOutputFormat::Jpeg {
quality: jpeg_quality,
}
_ => {
let jpeg_quality = 75;
ImageOutputFormat::Jpeg(jpeg_quality)
}
_ => ImageOutputFormat::Jpeg {
quality: jpeg_quality,
},
};
// We need to rewrite images with Exif to remove metadata such as location,
// camera model, etc.
@@ -479,18 +463,14 @@ impl<'a> BlobObject<'a> {
let do_scale = exceeds_max_bytes
|| strict_limits
&& (exceeds_wh
|| exif.is_some() && {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
encoded_img_exceeds_bytes(
|| exif.is_some()
&& encoded_img_exceeds_bytes(
context,
&img,
ofmt.clone(),
max_bytes,
&mut encoded,
)?
});
)?);
if do_scale {
if !exceeds_wh {
@@ -503,9 +483,6 @@ impl<'a> BlobObject<'a> {
}
loop {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(
@@ -539,7 +516,7 @@ impl<'a> BlobObject<'a> {
if do_scale || exif.is_some() {
// The file format is JPEG/PNG now, we may have to change the file extension
if !matches!(fmt, Ok(ImageFormat::Jpeg))
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
&& matches!(ofmt, ImageOutputFormat::Jpeg(_))
{
blob_abs = blob_abs.with_extension("jpg");
let file_name = blob_abs.file_name().context("No image file name (???)")?;
@@ -548,9 +525,6 @@ impl<'a> BlobObject<'a> {
}
if encoded.is_empty() {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
encode_img(&img, ofmt, &mut encoded)?;
}
@@ -694,13 +668,7 @@ fn encode_img(
) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
match fmt {
ImageOutputFormat::Png => img.write_to(&mut buf, ImageFormat::Png)?,
ImageOutputFormat::Jpeg { quality } => {
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
img.write_with_encoder(encoder)?;
}
}
img.write_to(&mut buf, fmt)?;
Ok(())
}
@@ -726,21 +694,11 @@ fn encoded_img_exceeds_bytes(
Ok(false)
}
/// Removes transparency from an image using a white background.
fn add_white_bg(img: &mut DynamicImage) {
for y in 0..img.height() {
for x in 0..img.width() {
let mut p = Rgba([255u8, 255, 255, 255]);
p.blend(&img.get_pixel(x, y));
img.put_pixel(x, y, p);
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use fs::File;
use image::Pixel;
use image::{GenericImageView, Pixel};
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
@@ -952,40 +910,6 @@ mod tests {
assert!(!stem.contains('?'));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_white_bg() {
let t = TestContext::new().await;
let bytes0 = include_bytes!("../test-data/image/logo.png").as_slice();
let bytes1 = include_bytes!("../test-data/image/avatar900x900.png").as_slice();
for (bytes, color) in [
(bytes0, [255u8, 255, 255, 255]),
(bytes1, [253u8, 198, 0, 255]),
] {
let avatar_src = t.dir.path().join("avatar.png");
fs::write(&avatar_src, bytes).await.unwrap();
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
let img_wh = 128;
let maybe_sticker = &mut false;
let strict_limits = true;
blob.recode_to_size(
&t,
blob.to_abs_path(),
maybe_sticker,
img_wh,
20_000,
strict_limits,
)
.unwrap();
tokio::task::block_in_place(move || {
let img = image::open(blob.to_abs_path()).unwrap();
assert!(img.width() == img_wh);
assert!(img.height() == img_wh);
assert_eq!(img.get_pixel(0, 0), Rgba(color));
});
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;
+80 -83
View File
@@ -2,6 +2,7 @@
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@@ -1789,10 +1790,18 @@ impl Chat {
update_msg_id: Option<MsgId>,
timestamp: i64,
) -> Result<MsgId> {
let mut new_references = "".into();
let mut to_id = 0;
let mut location_id = 0;
let new_rfc724_mid = create_outgoing_rfc724_mid();
let from = context.get_primary_self_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
Chattype::Group => Some(self.grpid.as_str()),
_ => None,
};
create_outgoing_rfc724_mid(grpid, &from)
};
if self.typ == Chattype::Single {
if let Some(id) = context
@@ -1830,72 +1839,58 @@ impl Chat {
// reset encrypt error state eg. for forwarding
msg.param.remove(Param::ErroneousE2ee);
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
// Set "References:" to identify the "thread" of the conversation.
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
let new_references;
if self.is_self_talk() {
// As self-talks are mainly used to transfer data between devices,
// we do not set In-Reply-To/References in this case.
new_references = String::new();
} else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
// We don't filter `OutPending` and `OutFailed` messages because the new message for
// which `parent_query()` is done may assume that it will be received in a context
// affected by those messages, e.g. they could add new members to a group and the
// new message will contain them in "To:". Anyway recipients must be prepared to
// orphaned references.
self
.id
.get_parent_mime_headers(context, MessageState::OutPending)
.await?
{
// "In-Reply-To:" is not changed if it is set manually.
// This does not affect "References:" header, it will contain "default parent" (the
// latest message in the thread) anyway.
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
msg.in_reply_to = Some(parent_rfc724_mid.clone());
}
// Use parent `In-Reply-To` as a fallback
// in case parent message has no `References` header
// as specified in RFC 5322:
// > If the parent message does not contain
// > a "References:" field but does have an "In-Reply-To:" field
// > containing a single message identifier, then the "References:" field
// > will contain the contents of the parent's "In-Reply-To:" field
// > followed by the contents of the parent's "Message-ID:" field (if
// > any).
let parent_references = if parent_references.is_empty() {
parent_in_reply_to
} else {
parent_references
};
// The whole list of messages referenced may be huge.
// Only take 2 recent references and add third from `In-Reply-To`.
let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
references_vec.reverse();
if !parent_rfc724_mid.is_empty()
&& !references_vec.contains(&parent_rfc724_mid.as_str())
// set "In-Reply-To:" to identify the message to which the composed message is a reply;
// set "References:" to identify the "thread" of the conversation;
// both according to RFC 5322 3.6.4, page 25
//
// as self-talks are mainly used to transfer data between devices,
// we do not set In-Reply-To/References in this case.
if !self.is_self_talk() {
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
// We don't filter `OutPending` and `OutFailed` messages because the new message for
// which `parent_query()` is done may assume that it will be received in a context
// affected by those messages, e.g. they could add new members to a group and the
// new message will contain them in "To:". Anyway recipients must be prepared to
// orphaned references.
self
.id
.get_parent_mime_headers(context, MessageState::OutPending)
.await?
{
references_vec.push(&parent_rfc724_mid)
}
// "In-Reply-To:" is not changed if it is set manually.
// This does not affect "References:" header, it will contain "default parent" (the
// latest message in the thread) anyway.
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
msg.in_reply_to = Some(parent_rfc724_mid.clone());
}
if references_vec.is_empty() {
// As a fallback, use our Message-ID,
// same as in the case of top-level message.
new_references = new_rfc724_mid.clone();
// the whole list of messages referenced may be huge;
// only use the oldest and the parent message
let parent_references = parent_references
.find(' ')
.and_then(|n| parent_references.get(..n))
.unwrap_or(&parent_references);
if !parent_references.is_empty() && !parent_rfc724_mid.is_empty() {
// angle brackets are added by the mimefactory later
new_references = format!("{parent_references} {parent_rfc724_mid}");
} else if !parent_references.is_empty() {
new_references = parent_references.to_string();
} else if !parent_in_reply_to.is_empty() && !parent_rfc724_mid.is_empty() {
new_references = format!("{parent_in_reply_to} {parent_rfc724_mid}");
} else if !parent_in_reply_to.is_empty() {
new_references = parent_in_reply_to;
} else {
// as a fallback, use our Message-ID, see reasoning below.
new_references = new_rfc724_mid.clone();
}
} else {
new_references = references_vec.join(" ");
// this is a top-level message, add our Message-ID as first reference.
// as we always try to extract the grpid also from `References:`-header,
// this allows group conversations also if smtp-server as outlook change `Message-ID:`-header
// (MUAs usually keep the first Message-ID in `References:`-header unchanged).
new_references = new_rfc724_mid.clone();
}
} else {
// This is a top-level message.
// Add our Message-ID as first references.
// This allows us to identify replies to our message even if
// email server such as Outlook changes `Message-ID:` header.
// MUAs usually keep the first Message-ID in `References:` header unchanged.
new_references = new_rfc724_mid.clone();
}
// add independent location to database
@@ -2698,9 +2693,7 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
}
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
if !msg.hidden {
context.emit_msgs_changed(msg.chat_id, msg.id);
}
context.emit_msgs_changed(msg.chat_id, msg.id);
if msg.param.exists(Param::SetLatitude) {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
@@ -2744,8 +2737,17 @@ async fn prepare_send_msg(
/// The caller has to interrupt SMTP loop or otherwise process new rows.
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let mimefactory = MimeFactory::from_msg(context, msg).await?;
let attach_selfavatar = mimefactory.attach_selfavatar;
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
Ok(attach_selfavatar) => attach_selfavatar,
Err(err) => {
warn!(context, "SMTP job cannot get selfavatar-state: {err:#}.");
false
}
};
let mimefactory = MimeFactory::from_msg(context, msg, attach_selfavatar).await?;
let mut recipients = mimefactory.recipients();
let from = context.get_primary_self_addr().await?;
@@ -2810,12 +2812,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
msg.chat_id.set_gossiped_timestamp(context, now).await?;
}
if rendered_msg.is_group {
msg.chat_id
.update_timestamp(context, Param::MemberListTimestamp, now)
.await?;
}
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
@@ -4155,7 +4151,7 @@ pub async fn add_device_msg_with_importance(
if let Some(msg) = msg {
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
let rfc724_mid = create_outgoing_rfc724_mid();
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
prepare_msg_blob(context, msg).await?;
let timestamp_sent = create_smeared_timestamp(context);
@@ -4295,7 +4291,7 @@ pub(crate) async fn add_info_msg_with_cmd(
parent: Option<&Message>,
from_id: Option<ContactId>,
) -> Result<MsgId> {
let rfc724_mid = create_outgoing_rfc724_mid();
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
let mut param = Params::new();
@@ -4478,8 +4474,9 @@ impl Context {
#[cfg(test)]
mod tests {
use super::*;
use crate::chatlist::get_archived_cnt;
use crate::chatlist::{get_archived_cnt, Chatlist};
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::contact::{Contact, ContactAddress};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{sync, TestContext, TestContextManager};
@@ -5935,11 +5932,11 @@ mod tests {
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 2);
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 0);
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 2);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
@@ -5956,7 +5953,7 @@ mod tests {
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
let msg = sent_msg.payload();
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
let msg = msg.replace("Chat-", "XXXX-");
assert_eq!(msg.match_indices("Chat-").count(), 0);
+1 -1
View File
@@ -416,7 +416,7 @@ impl Chatlist {
if chat.id.is_archived_link() {
Ok(Default::default())
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
} else {
Ok(Summary {
text: stock_str::no_messages(context).await,
+3 -12
View File
@@ -475,15 +475,6 @@ impl Context {
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Returns true if sentbox ("Sent" folder) should be watched.
pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SentboxWatch).await?
&& self
.get_config(Config::ConfiguredSentboxFolder)
.await?
.is_some())
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -574,9 +565,6 @@ impl Context {
_ => Default::default(),
};
self.set_config_internal(key, value).await?;
if key == Config::SentboxWatch {
self.last_full_folder_scan.lock().await.take();
}
Ok(())
}
@@ -785,9 +773,12 @@ fn get_config_keys_string() -> String {
#[cfg(test)]
mod tests {
use std::string::ToString;
use num_traits::FromPrimitive;
use super::*;
use crate::constants;
use crate::test_utils::{sync, TestContext, TestContextManager};
#[test]
+1
View File
@@ -2,6 +2,7 @@
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
use std::path::{Path, PathBuf};
+12 -11
View File
@@ -804,12 +804,6 @@ impl Context {
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert(
"save_mime_headers",
self.get_config_bool(Config::SaveMimeHeaders)
.await?
.to_string(),
);
res.insert(
"download_limit",
self.get_config_int(Config::DownloadLimit)
@@ -1349,14 +1343,20 @@ pub fn get_version_str() -> &'static str {
#[cfg(test)]
mod tests {
use std::time::Duration;
use anyhow::Context as _;
use strum::IntoEnumIterator;
use tempfile::tempdir;
use super::*;
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::message::{Message, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{get_chat_msg, TestContext};
@@ -1395,7 +1395,7 @@ mod tests {
\n\
hello\n",
contact.get_addr(),
create_outgoing_rfc724_mid()
create_outgoing_rfc724_mid(None, contact.get_addr())
);
println!("{msg}");
receive_imf(t, msg.as_bytes(), false).await.unwrap();
@@ -1583,14 +1583,14 @@ mod tests {
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();
assert!(info.contains_key("database_dir"));
assert!(info.get("database_dir").is_some());
}
#[test]
fn test_get_info_no_context() {
let info = get_info();
assert!(info.contains_key("deltachat_core_version"));
assert!(!info.contains_key("database_dir"));
assert!(info.get("deltachat_core_version").is_some());
assert!(info.get("database_dir").is_none());
assert_eq!(info.get("level").unwrap(), "awesome");
}
@@ -1611,6 +1611,7 @@ mod tests {
"mail_port",
"mail_security",
"notify_about_wrong_pw",
"save_mime_headers",
"self_reporting_id",
"selfstatus",
"send_server",
+37 -48
View File
@@ -27,8 +27,12 @@ pub fn try_decrypt(
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => return Ok(None),
Some(res) => res,
};
decrypt_part(
@@ -65,29 +69,28 @@ pub(crate) async fn prepare_decryption(
});
}
let autocrypt_header = if context.is_self_addr(from).await? {
None
} else if let Some(aheader_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
let autocrypt_header =
if let Some(autocrypt_header_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&autocrypt_header_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
} else {
None
};
} else {
None
};
let dkim_results = handle_authres(context, mail, from, message_time).await?;
let allow_aeap = get_encrypted_mime(mail).is_some();
let peerstate = get_autocrypt_peerstate(
context,
from,
@@ -95,7 +98,6 @@ pub(crate) async fn prepare_decryption(
message_time,
// Disallowing keychanges is disabled for now:
true, // dkim_results.allow_keychange,
allow_aeap,
)
.await?;
@@ -124,13 +126,6 @@ pub struct DecryptionInfo {
pub(crate) dkim_results: authres::DkimResults,
}
/// Returns a reference to the encrypted payload of a message.
fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
}
/// Returns a reference to the encrypted payload of a ["Mixed
/// Up"][pgpmime-message-mangling] message.
///
@@ -269,7 +264,6 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
}
}
/// Returns public keyring for `peerstate`.
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<SignedPublicKey> {
let mut public_keyring_for_validate = Vec::new();
if let Some(peerstate) = peerstate {
@@ -297,28 +291,23 @@ pub(crate) async fn get_autocrypt_peerstate(
autocrypt_header: Option<&Aheader>,
message_time: i64,
allow_change: bool,
allow_aeap: bool,
) -> Result<Option<Peerstate>> {
let allow_change = allow_change && !context.is_self_addr(from).await?;
let mut peerstate;
// Apply Autocrypt header
if let Some(header) = autocrypt_header {
if allow_aeap {
// If we know this fingerprint from another addr,
// we may want to do a transition from this other addr
// (and keep its peerstate)
// For security reasons, for now, we only do a transition
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
from,
)
.await?;
} else {
peerstate = Peerstate::from_addr(context, from).await?;
}
// The "from_verified_fingerprint" part is for AEAP:
// If we know this fingerprint from another addr,
// we may want to do a transition from this other addr
// (and keep its peerstate)
// For security reasons, for now, we only do a transition
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
from,
)
.await?;
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {
+1 -8
View File
@@ -301,7 +301,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
let href = href
.decode_and_unescape_value(reader)
.unwrap_or_default()
.to_string();
.to_lowercase();
if !href.is_empty() {
dehtml.last_href = Some(href);
@@ -463,13 +463,6 @@ mod tests {
assert_eq!(plain, "[text](url)");
}
#[test]
fn test_dehtml_case_sensitive_link() {
let html = "<html><A HrEf=\"https://foo.bar/Data\">case in URLs matter</A></html>";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "[case in URLs matter](https://foo.bar/Data)");
}
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
+1
View File
@@ -253,6 +253,7 @@ mod tests {
use super::*;
use crate::chat::{get_chat_msgs, send_msg};
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::receive_imf::receive_imf_from_inbox;
use crate::test_utils::TestContext;
+1 -2
View File
@@ -95,7 +95,6 @@ impl EncryptHelper {
verified: bool,
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, String)>,
compress: bool,
) -> Result<String> {
let mut keyring: Vec<SignedPublicKey> = Vec::new();
@@ -136,7 +135,7 @@ impl EncryptHelper {
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?;
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key)).await?;
Ok(ctext)
}
+7 -19
View File
@@ -64,7 +64,7 @@
use std::cmp::max;
use std::collections::BTreeSet;
use std::fmt;
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, UNIX_EPOCH};
@@ -131,9 +131,9 @@ impl Default for Timer {
}
}
impl fmt::Display for Timer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_u32())
impl ToString for Timer {
fn to_string(&self) -> String {
self.to_u32().to_string()
}
}
@@ -569,21 +569,9 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
"Ephemeral loop waiting for deletion in {} or interrupt",
duration_to_str(duration)
);
match timeout(duration, interrupt_receiver.recv()).await {
Ok(Ok(())) => {
// received an interruption signal, recompute waiting time (if any)
continue;
}
Ok(Err(err)) => {
warn!(
context,
"Interrupt channel closed, ephemeral loop exits now: {err:#}."
);
return;
}
Err(_err) => {
// Timeout.
}
if timeout(duration, interrupt_receiver.recv()).await.is_ok() {
// received an interruption signal, recompute waiting time (if any)
continue;
}
}
+4 -1
View File
@@ -107,7 +107,10 @@ pub enum EventType {
},
/// Downloading a bunch of messages just finished.
IncomingMsgBunch,
IncomingMsgBunch {
/// List of incoming message IDs.
msg_ids: Vec<MsgId>,
},
/// Messages were seen or noticed.
/// chat id is always set.
-5
View File
@@ -74,11 +74,6 @@ pub enum HeaderDef {
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
/// Deprecated header containing Group-ID in `vg-request-with-auth` message.
///
/// It is not used by Alice as Alice knows the group corresponding to the AUTH token.
/// Bob still sends it for backwards compatibility.
SecureJoinGroup,
SecureJoinFingerprint,
SecureJoinInvitenumber,
+92 -78
View File
@@ -7,9 +7,13 @@
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
use std::future::Future;
use std::pin::Pin;
use anyhow::{Context as _, Result};
use base64::Engine as _;
use lettre_email::mime::Mime;
use futures::future::FutureExt;
use lettre_email::mime::{self, Mime};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
@@ -63,7 +67,7 @@ enum MimeMultipartType {
/// and checks and returns the rough mime-type.
fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
let mimetype = ctype.mimetype.to_lowercase();
if mimetype.starts_with("multipart") && ctype.params.contains_key("boundary") {
if mimetype.starts_with("multipart") && ctype.params.get("boundary").is_some() {
MimeMultipartType::Multiple
} else if mimetype == "message/rfc822" {
MimeMultipartType::Message
@@ -112,109 +116,119 @@ impl HtmlMsgParser {
/// Usually, there is at most one plain-text and one HTML-text part,
/// multiple plain-text parts might be used for mailinglist-footers,
/// therefore we use the first one.
async fn collect_texts_recursive<'a>(
fn collect_texts_recursive<'a>(
&'a mut self,
mail: &'a mailparse::ParsedMail<'a>,
) -> Result<()> {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
Box::pin(self.collect_texts_recursive(cur_data)).await?
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
async move {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
self.collect_texts_recursive(cur_data).await?
}
Ok(())
}
Ok(())
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.collect_texts_recursive(&mail).await
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
Box::pin(self.collect_texts_recursive(&mail)).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if self.html.is_empty() {
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if self.html.is_empty() {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
}
}
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
},
});
}
}
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
if let Ok(decoded_data) = mail.get_body() {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
},
});
}
Ok(())
}
Ok(())
}
}
.boxed()
}
/// Replace cid:-protocol by the data:-protocol where appropriate.
/// This allows the final html-file to be self-contained.
async fn cid_to_data_recursive<'a>(
fn cid_to_data_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
) -> Result<()> {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
Box::pin(self.cid_to_data_recursive(context, cur_data)).await?;
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
async move {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
self.cid_to_data_recursive(context, cur_data).await?;
}
Ok(())
}
Ok(())
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.cid_to_data_recursive(context, &mail).await
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
Box::pin(self.cid_to_data_recursive(context, &mail)).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::IMAGE {
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
if let Ok(cid) = parse_message_id(&cid) {
if let Ok(replacement) = mimepart_to_data_url(mail) {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
format!("${{1}}{replacement}${{3}}").as_str(),
)
.as_ref()
.to_string()
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::IMAGE {
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
if let Ok(cid) = parse_message_id(&cid) {
if let Ok(replacement) = mimepart_to_data_url(mail) {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
format!("${{1}}{replacement}${{3}}").as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}",
re_string,
e
),
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}", re_string, e
),
}
}
}
}
Ok(())
}
Ok(())
}
}
.boxed()
}
}
+9 -2
View File
@@ -717,8 +717,12 @@ impl Imap {
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
if !received_msgs.is_empty() {
context.emit_event(EventType::IncomingMsgBunch);
let msg_ids: Vec<MsgId> = received_msgs
.iter()
.flat_map(|m| m.msg_ids.clone())
.collect();
if !msg_ids.is_empty() {
context.emit_event(EventType::IncomingMsgBunch { msg_ids });
}
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
@@ -2410,6 +2414,9 @@ async fn add_all_recipients_as_contacts(
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::ChatId;
use crate::config::Config;
use crate::contact::Contact;
use crate::test_utils::TestContext;
#[test]
+5 -62
View File
@@ -10,7 +10,6 @@ use futures::StreamExt;
use futures_lite::FutureExt;
use rand::{thread_rng, Rng};
use tokio::fs::{self, File};
use tokio::io::BufWriter;
use tokio_tar::Archive;
use crate::blob::{BlobDirContents, BlobObject};
@@ -500,7 +499,7 @@ fn get_next_backup_path(
backup_time: i64,
) -> Result<(PathBuf, PathBuf, PathBuf)> {
let folder = PathBuf::from(folder);
let stem = chrono::DateTime::<chrono::Utc>::from_timestamp(backup_time, 0)
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
.context("can't get next backup path")?
// Don't change this file name format, in `dc_imex_has_backup` we use string comparison to determine which backup is newer:
.format("delta-chat-backup-%Y-%m-%d")
@@ -817,20 +816,6 @@ async fn export_database(
.await
}
/// Serializes the database to a file.
pub async fn serialize_database(context: &Context, filename: &str) -> Result<()> {
let file = File::create(filename).await?;
context.sql.serialize(BufWriter::new(file)).await?;
Ok(())
}
/// Deserializes the database from a file.
pub async fn deserialize_database(context: &Context, filename: &str) -> Result<()> {
let file = File::open(filename).await?;
context.sql.deserialize(file).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::time::Duration;
@@ -839,9 +824,10 @@ mod tests {
use tokio::task;
use super::*;
use crate::key;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::stock_str::StockMessage;
use crate::test_utils::{alice_keypair, TestContext, TestContextManager};
use crate::test_utils::{alice_keypair, TestContext};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file() {
@@ -1109,17 +1095,13 @@ mod tests {
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!(headers.get(HEADER_AUTOCRYPT).is_none());
assert!(!base64.is_empty());
@@ -1133,24 +1115,7 @@ mod tests {
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.to_string();
let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
assert!(decrypt_setup_file(
incorrect_setupcode,
std::io::Cursor::new(setup_file.as_bytes()),
)
.await
.is_err());
assert!(headers.get(HEADER_SETUPCODE).is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1168,7 +1133,6 @@ mod tests {
alice2.configure_addr("alice@example.org").await;
alice2.recv_msg(&sent).await;
let msg = alice2.get_last_msg().await;
assert!(msg.is_setupmessage());
// Send a message that cannot be decrypted because the keys are
// not synchronized yet.
@@ -1186,25 +1150,4 @@ mod tests {
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(())
}
}
+1
View File
@@ -597,6 +597,7 @@ mod tests {
use std::time::Duration;
use crate::chat::{get_chat_msgs, send_msg, ChatItem};
use crate::message::{Message, Viewtype};
use crate::test_utils::TestContextManager;
use super::*;
-19
View File
@@ -101,25 +101,6 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPubl
}
}
/// Returns our own public keyring.
pub(crate) async fn load_self_public_keyring(context: &Context) -> Result<Vec<SignedPublicKey>> {
let keys = context
.sql
.query_map(
r#"SELECT public_key
FROM keypairs
ORDER BY id=(SELECT value FROM config WHERE keyname='key_id') DESC"#,
(),
|row| row.get::<_, Vec<u8>>(0),
|keys| keys.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?
.into_iter()
.filter_map(|bytes| SignedPublicKey::from_slice(&bytes).log_err(context).ok())
.collect();
Ok(keys)
}
pub(crate) async fn load_self_secret_key(context: &Context) -> Result<SignedSecretKey> {
let private_key = context
.sql
+4 -17
View File
@@ -1,5 +1,6 @@
//! Location handling.
use std::convert::TryFrom;
use std::time::Duration;
use anyhow::{ensure, Context as _, Result};
@@ -137,7 +138,7 @@ impl Kml {
// 0 4 7 10 13 16 19
match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") {
Ok(res) => {
self.curr.timestamp = res.and_utc().timestamp();
self.curr.timestamp = res.timestamp();
let now = time();
if self.curr.timestamp > now {
self.curr.timestamp = now;
@@ -540,7 +541,7 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(Strin
fn get_kml_timestamp(utc: i64) -> String {
// Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC.
chrono::DateTime::<chrono::Utc>::from_timestamp(utc, 0)
chrono::NaiveDateTime::from_timestamp_opt(utc, 0)
.unwrap()
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string()
@@ -679,21 +680,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive
"Location loop is waiting for {} or interrupt",
duration_to_str(duration)
);
match timeout(duration, interrupt_receiver.recv()).await {
Err(_err) => {
info!(context, "Location loop timeout.");
}
Ok(Err(err)) => {
warn!(
context,
"Interrupt channel closed, location loop exits now: {err:#}."
);
return;
}
Ok(Ok(())) => {
info!(context, "Location loop received interrupt.");
}
}
timeout(duration, interrupt_receiver.recv()).await.ok();
}
}
+10 -24
View File
@@ -201,14 +201,12 @@ WHERE id=?;
let fts = timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {fts}");
let from_contact = Contact::get_by_id(context, msg.from_id).await?;
let name = from_contact.get_name_n_addr();
if let Some(override_sender_name) = msg.get_override_sender_name() {
let addr = from_contact.get_addr();
ret += &format!(" by ~{override_sender_name} ({addr})");
} else {
ret += &format!(" by {name}");
}
let name = Contact::get_by_id(context, msg.from_id)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
ret += &format!(" by {name}");
ret += "\n";
if msg.from_id != ContactId::SELF {
@@ -459,19 +457,7 @@ impl Message {
}
/// Loads message with given ID from the database.
///
/// Returns an error if the message does not exist.
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
let message = Self::load_from_db_optional(context, id)
.await?
.context("Message {id} does not exist")?;
Ok(message)
}
/// Loads message with given ID from the database.
///
/// Returns `None` if the message does not exist.
pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
ensure!(
!id.is_special(),
"Can not load special message ID {} from DB",
@@ -479,7 +465,7 @@ impl Message {
);
let msg = context
.sql
.query_row_optional(
.query_row(
concat!(
"SELECT",
" m.id AS id,",
@@ -808,7 +794,7 @@ impl Message {
None
};
Summary::new(context, self, chat, contact.as_ref()).await
Ok(Summary::new(context, self, chat, contact.as_ref()).await)
}
// It's a little unfortunate that the UI has to first call `dc_msg_get_override_sender_name` and then if it was `NULL`, call
@@ -2030,7 +2016,7 @@ mod tests {
assert_eq!(_msg2.get_filemime(), None);
}
/// Tests that message can be prepared even if account has no configured address.
/// Tests that message cannot be prepared if account has no configured address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prepare_not_configured() {
let d = test::TestContext::new().await;
@@ -2040,7 +2026,7 @@ mod tests {
let mut msg = Message::new(Viewtype::Text);
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_ok());
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+33 -73
View File
@@ -1,16 +1,17 @@
//! # MIME message production.
use std::collections::HashSet;
use std::convert::TryInto;
use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use chrono::TimeZone;
use format_flowed::{format_flowed, format_flowed_quote};
use lettre_email::{Address, Header, MimeMultipartType, PartBuilder};
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use tokio::fs;
use crate::blob::BlobObject;
use crate::chat::{self, Chat};
use crate::chat::Chat;
use crate::config::Config;
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::Contact;
@@ -64,15 +65,7 @@ pub struct MimeFactory<'a> {
loaded: Loaded,
msg: &'a Message,
in_reply_to: String,
/// Space-separated list of Message-IDs for `References` header.
///
/// Each Message-ID in the list
/// may or may not be enclosed in angle brackets,
/// angle brackets must be added during message rendering
/// as needed.
references: String,
req_mdn: bool,
last_added_location_id: Option<u32>,
@@ -83,7 +76,7 @@ pub struct MimeFactory<'a> {
sync_ids_to_delete: Option<String>,
/// True if the avatar should be attached.
pub attach_selfavatar: bool,
attach_selfavatar: bool,
}
/// Result of rendering a message, ready to be submitted to a send job.
@@ -93,7 +86,6 @@ pub struct RenderedEmail {
// pub envelope: Envelope,
pub is_encrypted: bool,
pub is_gossiped: bool,
pub is_group: bool,
pub last_added_location_id: Option<u32>,
/// A comma-separated string of sync-IDs that are used by the rendered email
@@ -138,7 +130,11 @@ struct MessageHeaders {
}
impl<'a> MimeFactory<'a> {
pub async fn from_msg(context: &Context, msg: &'a Message) -> Result<MimeFactory<'a>> {
pub async fn from_msg(
context: &Context,
msg: &'a Message,
attach_selfavatar: bool,
) -> Result<MimeFactory<'a>> {
let chat = Chat::load_from_db(context, msg.chat_id).await?;
let from_addr = context.get_primary_self_addr().await?;
@@ -213,7 +209,6 @@ impl<'a> MimeFactory<'a> {
},
)
.await?;
let attach_selfavatar = Self::should_attach_selfavatar(context, msg).await;
let factory = MimeFactory {
from_addr,
@@ -364,56 +359,26 @@ impl<'a> MimeFactory<'a> {
}
}
async fn should_do_gossip(&self, context: &Context, multiple_recipients: bool) -> Result<bool> {
async fn should_do_gossip(&self, context: &Context) -> Result<bool> {
match &self.loaded {
Loaded::Message { chat } => {
let cmd = self.msg.param.get_cmd();
if cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage
{
// beside key- and member-changes, force a periodic re-gossip.
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
if time() >= gossiped_timestamp + gossip_period {
Ok(true)
} else if multiple_recipients {
// beside key- and member-changes, force a periodic re-gossip.
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
if time() >= gossiped_timestamp + gossip_period {
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
let cmd = self.msg.param.get_cmd();
// Do gossip in all Securejoin messages not to complicate the code. There's no
// need in gossips in "vg-auth-required" messages f.e., but let them be.
Ok(cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage)
}
}
Loaded::Mdn { .. } => Ok(false),
}
}
async fn should_attach_selfavatar(context: &Context, msg: &Message) -> bool {
let cmd = msg.param.get_cmd();
(cmd != SystemMessage::SecurejoinMessage || {
let step = msg.param.get(Param::Arg).unwrap_or_default();
// Don't attach selfavatar at the earlier SecureJoin steps:
// - The corresponding messages, i.e. "v{c,g}-request" and "v{c,g}-auth-required" are
// deleted right after processing, so other devices won't see the avatar.
// - It's also good for privacy because the contact isn't yet verified and these
// messages are auto-sent unlike usual unencrypted messages.
step == "vg-request-with-auth"
|| step == "vc-request-with-auth"
|| step == "vg-member-added"
|| step == "vc-contact-confirm"
}) && match chat::shall_attach_selfavatar(context, msg.chat_id).await {
Ok(should) => should,
Err(err) => {
warn!(
context,
"should_attach_selfavatar: cannot get selfavatar state: {err:#}."
);
false
}
}
}
fn grpimage(&self) -> Option<String> {
match &self.loaded {
Loaded::Message { chat } => {
@@ -575,14 +540,18 @@ impl<'a> MimeFactory<'a> {
.protected
.push(Header::new("Subject".into(), encoded_subject));
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp, 0)
let date = chrono::Utc
.from_local_datetime(
&chrono::NaiveDateTime::from_timestamp_opt(self.timestamp, 0)
.context("can't convert timestamp to NativeDateTime")?,
)
.unwrap()
.to_rfc2822();
headers.unprotected.push(Header::new("Date".into(), date));
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(None, &self.from_addr),
};
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue);
@@ -614,8 +583,6 @@ impl<'a> MimeFactory<'a> {
));
}
let mut is_group = false;
if let Loaded::Message { chat } = &self.loaded {
if chat.typ == Chattype::Broadcast {
let encoded_chat_name = encode_words(&chat.name);
@@ -623,8 +590,6 @@ impl<'a> MimeFactory<'a> {
"List-ID".into(),
format!("{encoded_chat_name} <{}>", chat.grpid),
));
} else if chat.typ == Chattype::Group {
is_group = true;
}
}
@@ -733,9 +698,9 @@ impl<'a> MimeFactory<'a> {
.fold(message, |message, header| message.header(header));
// Add gossip headers in chats with multiple recipients
let multiple_recipients =
peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
if self.should_do_gossip(context, multiple_recipients).await? {
if (peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?)
&& self.should_do_gossip(context).await?
{
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if let Some(header) = peerstate.render_gossip_header(verified) {
message = message.header(Header::new("Autocrypt-Gossip".into(), header));
@@ -769,12 +734,8 @@ impl<'a> MimeFactory<'a> {
);
}
// Disable compression for SecureJoin to ensure
// there are no compression side channels
// leaking information about the tokens.
let compress = self.msg.param.get_cmd() != SystemMessage::SecurejoinMessage;
let encrypted = encrypt_helper
.encrypt(context, verified, message, peerstates, compress)
.encrypt(context, verified, message, peerstates)
.await?;
outer_message
@@ -896,7 +857,6 @@ impl<'a> MimeFactory<'a> {
// envelope: Envelope::new,
is_encrypted,
is_gossiped,
is_group,
last_added_location_id,
sync_ids_to_delete: self.sync_ids_to_delete,
rfc724_mid,
@@ -1854,7 +1814,7 @@ mod tests {
Original-Message-ID: <2893@example.com>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n", &t).await;
let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap();
let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap();
// The subject string should not be "Re: message opened"
assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap());
}
@@ -1989,7 +1949,7 @@ mod tests {
new_msg.chat_id = chat_id;
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap();
let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap();
mf.subject_str(&t).await.unwrap()
}
@@ -2074,7 +2034,7 @@ mod tests {
new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap();
}
let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap();
let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap();
mf.subject_str(&t).await.unwrap()
}
@@ -2122,7 +2082,7 @@ mod tests {
)
.await;
let mimefactory = MimeFactory::from_msg(&t, &msg).await.unwrap();
let mimefactory = MimeFactory::from_msg(&t, &msg, false).await.unwrap();
let recipients = mimefactory.recipients();
assert_eq!(recipients, vec!["charlie@example.com"]);
+59 -120
View File
@@ -2,13 +2,15 @@
use std::cmp::min;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::str;
use anyhow::{bail, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use format_flowed::unformat_flowed;
use lettre_email::mime::Mime;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use crate::aheader::{Aheader, EncryptPreference};
@@ -25,7 +27,7 @@ use crate::decrypt::{
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
use crate::key::{load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
use crate::message::{
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
};
@@ -273,18 +275,6 @@ impl MimeMessage {
}
}
// Overwrite Message-ID with X-Microsoft-Original-Message-ID.
// However if we later find Message-ID in the protected part,
// it will overwrite both.
if let Some(microsoft_message_id) =
headers.remove(HeaderDef::XMicrosoftOriginalMessageId.get_headername())
{
headers.insert(
HeaderDef::MessageId.get_headername().to_string(),
microsoft_message_id,
);
}
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
// them in signed-only emails, but has no value currently.
Self::remove_secured_headers(&mut headers);
@@ -302,11 +292,7 @@ impl MimeMessage {
hop_info += "\n\n";
hop_info += &decryption_info.dkim_results.to_string();
let incoming = !context.is_self_addr(&from.addr).await?;
let public_keyring = match decryption_info.peerstate.is_none() && !incoming {
true => key::load_self_public_keyring(context).await?,
false => keyring_from_peerstate(decryption_info.peerstate.as_ref()),
};
let public_keyring = keyring_from_peerstate(decryption_info.peerstate.as_ref());
let (mail, mut signatures, encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &public_keyring)
}) {
@@ -349,20 +335,9 @@ impl MimeMessage {
gossip_headers,
)
.await?;
// Remove unsigned opportunistically protected headers from messages considered
// Autocrypt-encrypted / displayed with padlock.
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
for h in [
HeaderDef::Subject,
HeaderDef::ChatGroupId,
HeaderDef::ChatGroupName,
HeaderDef::ChatGroupNameChanged,
HeaderDef::ChatGroupAvatar,
HeaderDef::ChatGroupMemberRemoved,
HeaderDef::ChatGroupMemberAdded,
] {
headers.remove(h.get_headername());
}
// Remove unsigned subject from messages displayed with padlock.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
headers.remove("subject");
}
// let known protected headers from the decrypted
@@ -390,20 +365,13 @@ impl MimeMessage {
// signed part, but it doesn't match the outer one.
// This _might_ be because the sender's mail server
// replaced the sending address, e.g. in a mailing list.
// Or it's because someone is doing some replay attack.
// Resending encrypted messages via mailing lists
// without reencrypting is not useful anyway,
// so we return an error below.
// Or it's because someone is doing some replay attack
// - OTOH, I can't come up with an attack scenario
// where this would be useful.
warn!(
context,
"From header in signed part doesn't match the outer one",
);
// Return an error from the parser.
// This will result in creating a tombstone
// and no further message processing
// as if the MIME structure is broken.
bail!("From header is forged");
}
}
}
@@ -431,6 +399,7 @@ impl MimeMessage {
}
}
let incoming = !context.is_self_addr(&from.addr).await?;
let mut parser = MimeMessage {
parts: Vec::new(),
headers,
@@ -521,7 +490,7 @@ impl MimeMessage {
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts.retain(|part| {
part.mimetype.is_none()
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
@@ -807,7 +776,6 @@ impl MimeMessage {
pub(crate) fn get_subject(&self) -> Option<String> {
self.get_header(HeaderDef::Subject)
.map(|s| s.trim_start())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
}
@@ -816,53 +784,59 @@ impl MimeMessage {
self.headers.get(headerdef.get_headername())
}
async fn parse_mime_recursive<'a>(
fn parse_mime_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
is_related: bool,
) -> Result<bool> {
enum MimeS {
Multiple,
Single,
Message,
}
) -> Pin<Box<dyn Future<Output = Result<bool>> + 'a + Send>> {
use futures::future::FutureExt;
let mimetype = mail.ctype.mimetype.to_lowercase();
let m = if mimetype.starts_with("multipart") {
if mail.ctype.params.contains_key("boundary") {
MimeS::Multiple
} else {
MimeS::Single
// Boxed future to deal with recursion
async move {
enum MimeS {
Multiple,
Single,
Message,
}
} else if mimetype.starts_with("message") {
if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
MimeS::Message
} else {
MimeS::Single
}
} else {
MimeS::Single
};
let is_related = is_related || mimetype == "multipart/related";
match m {
MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(false);
let mimetype = mail.ctype.mimetype.to_lowercase();
let m = if mimetype.starts_with("multipart") {
if mail.ctype.params.get("boundary").is_some() {
MimeS::Multiple
} else {
MimeS::Single
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
} else if mimetype.starts_with("message") {
if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
MimeS::Message
} else {
MimeS::Single
}
} else {
MimeS::Single
};
Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
}
MimeS::Single => {
self.add_single_part_if_known(context, mail, is_related)
.await
let is_related = is_related || mimetype == "multipart/related";
match m {
MimeS::Multiple => self.handle_multiple(context, mail, is_related).await,
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(false);
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.parse_mime_recursive(context, &mail, is_related).await
}
MimeS::Single => {
self.add_single_part_if_known(context, mail, is_related)
.await
}
}
}
.boxed()
}
async fn handle_multiple(
@@ -1405,22 +1379,14 @@ impl MimeMessage {
}
pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
self.get_header(HeaderDef::MessageId)
self.get_header(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get_header(HeaderDef::MessageId))
.and_then(|msgid| parse_message_id(msgid).ok())
}
fn remove_secured_headers(headers: &mut HashMap<String, String>) {
headers.remove("secure-join-fingerprint");
headers.remove("secure-join-auth");
headers.remove("chat-verified");
headers.remove("autocrypt-gossip");
// Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
if let Some(secure_join) = headers.remove("secure-join") {
if secure_join == "vc-request" || secure_join == "vg-request" {
headers.insert("secure-join".to_string(), secure_join);
}
}
}
fn merge_headers(
@@ -1845,8 +1811,6 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
}
}
/// Returns true if the header overwrites outer header
/// when it comes from protected headers.
fn is_known(key: &str) -> bool {
matches!(
key,
@@ -1862,7 +1826,6 @@ fn is_known(key: &str) -> bool {
| "in-reply-to"
| "references"
| "subject"
| "secure-join"
)
}
@@ -2252,12 +2215,11 @@ mod tests {
use super::*;
use crate::{
chat,
chatlist::Chatlist,
constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
message::MessengerMessage,
message::{Message, MessageState, MessengerMessage},
receive_imf::receive_imf,
test_utils::{TestContext, TestContextManager},
test_utils::TestContext,
tools::time,
};
@@ -3576,29 +3538,6 @@ On 2020-10-25, Bob wrote:
);
}
/// Tests that X-Microsoft-Original-Message-ID does not overwrite encrypted Message-ID.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id;
chat::send_text_msg(&bob, bob_chat_id, "hi!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
// Insert X-Microsoft-Original-Message-ID.
// It should be ignored because there is a Message-ID in the encrypted part.
sent_msg.payload = sent_msg.payload.replace(
"Message-ID:",
"X-Microsoft-Original-Message-ID: <fake-message-id@example.net>\r\nMessage-ID:",
);
let msg = alice.recv_msg(&sent_msg).await;
assert!(!msg.rfc724_mid.contains("fake-message-id"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_in_reply_to() -> Result<()> {
let t = TestContext::new_alice().await;
+7 -69
View File
@@ -1,6 +1,6 @@
//! # Common network utilities.
use std::net::Ipv4Addr;
use std::net::{IpAddr, SocketAddr};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::pin::Pin;
use std::str::FromStr;
use std::time::Duration;
@@ -41,8 +41,7 @@ async fn lookup_host_with_timeout(
/// Looks up hostname and port using DNS and updates the address resolution cache.
///
/// If `load_cache` is true, appends cached results not older than 30 days to the end
/// or entries from fallback cache if there are no cached addresses.
/// If `load_cache` is true, appends cached results not older than 30 days to the end.
async fn lookup_host_with_cache(
context: &Context,
hostname: &str,
@@ -130,72 +129,11 @@ async fn lookup_host_with_cache(
//
// In the future we may pre-resolve all provider database addresses
// and build them in.
match hostname {
"mail.sangham.net" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
port,
));
}
"nine.testrun.org" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
port,
));
}
"disroot.org" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139)),
port,
));
}
"mail.riseup.net" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)),
port,
));
}
"imap.gmail.com" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
port,
));
}
"smtp.gmail.com" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
port,
));
}
_ => {}
if hostname == "mail.sangham.net" {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
port,
));
}
}
}
+21 -14
View File
@@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
use crate::blob::BlobObject;
use crate::context::Context;
use crate::message::MsgId;
use crate::mimeparser::SystemMessage;
/// Available param keys.
@@ -17,7 +18,7 @@ use crate::mimeparser::SystemMessage;
)]
#[repr(u8)]
pub enum Param {
/// For messages
/// For messages and jobs
File = b'f',
/// For messages: original filename (as shown in chat)
@@ -64,15 +65,6 @@ pub enum Param {
/// For Messages: the message is a reaction.
Reaction = b'x',
/// For Chats: the timestamp of the last reaction.
LastReactionTimestamp = b'y',
/// For Chats: Message ID of the last reaction.
LastReactionMsgId = b'Y',
/// For Chats: Contact ID of the last reaction.
LastReactionContactId = b'1',
/// For Messages: a message with "Auto-Submitted: auto-generated" header ("bot").
Bot = b'b',
@@ -95,7 +87,7 @@ pub enum Param {
/// `Secure-Join-Fingerprint` header for `{vc,vg}-request-with-auth` messages.
Arg3 = b'G',
/// Deprecated `Secure-Join-Group` header for messages.
/// For Messages
Arg4 = b'H',
/// For Messages
@@ -115,12 +107,18 @@ pub enum Param {
/// is used to also send all the forwarded messages.
PrepForwards = b'P',
/// For Messages
/// For Jobs
SetLatitude = b'l',
/// For Messages
/// For Jobs
SetLongitude = b'n',
/// For Jobs
AlsoMove = b'M',
/// For MDN-sending job
MsgId = b'I',
/// For Groups
///
/// An unpromoted group has not had any messages sent to it and thus only exists on the
@@ -175,6 +173,9 @@ pub enum Param {
/// For Chats: timestamp of member list update.
MemberListTimestamp = b'k',
/// For Chats: timestamp of protection settings update.
ProtectionSettingsTimestamp = b'L',
/// For Webxdc Message Instances: Current document name
WebxdcDocument = b'R',
@@ -189,7 +190,6 @@ pub enum Param {
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
ForceSticker = b'X',
// 'L' was defined as ProtectionSettingsTimestamp for Chats, however, never used in production.
}
/// An object for handling key=value parameter lists.
@@ -399,6 +399,12 @@ impl Params {
Ok(Some(path))
}
pub fn get_msg_id(&self) -> Option<MsgId> {
self.get(Param::MsgId)
.and_then(|x| x.parse().ok())
.map(MsgId::new)
}
/// Set the given parameter to the passed in `i32`.
pub fn set_int(&mut self, key: Param, value: i32) -> &mut Self {
self.set(key, format!("{value}"));
@@ -449,6 +455,7 @@ mod tests {
use std::path::Path;
use std::str::FromStr;
use anyhow::Result;
use tokio::fs;
use super::*;
+88 -96
View File
@@ -1,7 +1,5 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module.
use std::mem;
use anyhow::{Context as _, Error, Result};
use num_traits::FromPrimitive;
@@ -167,9 +165,6 @@ impl Peerstate {
/// Loads peerstate corresponding to the given address from the database.
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
if context.is_self_addr(addr).await? {
return Ok(None);
}
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, \
@@ -187,7 +182,6 @@ impl Peerstate {
context: &Context,
fingerprint: &Fingerprint,
) -> Result<Option<Peerstate>> {
// NOTE: If it's our key fingerprint, this returns None currently.
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, \
@@ -212,9 +206,6 @@ impl Peerstate {
fingerprint: &Fingerprint,
addr: &str,
) -> Result<Option<Peerstate>> {
if context.is_self_addr(addr).await? {
return Ok(None);
}
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, \
@@ -225,10 +216,9 @@ impl Peerstate {
FROM acpeerstates \
WHERE verified_key_fingerprint=? \
OR addr=? COLLATE NOCASE \
ORDER BY verified_key_fingerprint=? DESC, addr=? COLLATE NOCASE DESC, \
last_seen DESC LIMIT 1;";
ORDER BY verified_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, (&fp, addr, &fp, addr)).await
Self::from_stmt(context, query, (&fp, &addr, &fp)).await
}
async fn from_stmt(
@@ -526,82 +516,65 @@ impl Peerstate {
/// Saves the peerstate to the database.
pub async fn save_to_db(&self, sql: &Sql) -> Result<()> {
self.save_to_db_ex(sql, None).await
}
/// Saves the peerstate to the database.
///
/// * `old_addr`: Old address of the peerstate in case of an AEAP transition.
pub(crate) async fn save_to_db_ex(&self, sql: &Sql, old_addr: Option<&str>) -> Result<()> {
let trans_fn = |t: &mut rusqlite::Transaction| {
if let Some(old_addr) = old_addr {
// We are doing an AEAP transition to the new address and the SQL INSERT below will
// save the existing peerstate as belonging to this new address. We now need to
// delete the peerstate that belongs to the current address in case if the contact
// later wants to move back to the current address. Otherwise the old entry will be
// just found and updated instead of doing AEAP.
t.execute("DELETE FROM acpeerstates WHERE addr=?", (old_addr,))?;
}
t.execute(
"INSERT INTO acpeerstates (
last_seen,
last_seen_autocrypt,
prefer_encrypted,
public_key,
gossip_timestamp,
gossip_key,
public_key_fingerprint,
gossip_key_fingerprint,
verified_key,
verified_key_fingerprint,
verifier,
secondary_verified_key,
secondary_verified_key_fingerprint,
secondary_verifier,
backward_verified_key_id,
addr)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT (addr)
DO UPDATE SET
last_seen = excluded.last_seen,
last_seen_autocrypt = excluded.last_seen_autocrypt,
prefer_encrypted = excluded.prefer_encrypted,
public_key = excluded.public_key,
gossip_timestamp = excluded.gossip_timestamp,
gossip_key = excluded.gossip_key,
public_key_fingerprint = excluded.public_key_fingerprint,
gossip_key_fingerprint = excluded.gossip_key_fingerprint,
verified_key = excluded.verified_key,
verified_key_fingerprint = excluded.verified_key_fingerprint,
verifier = excluded.verifier,
secondary_verified_key = excluded.secondary_verified_key,
secondary_verified_key_fingerprint = excluded.secondary_verified_key_fingerprint,
secondary_verifier = excluded.secondary_verifier,
backward_verified_key_id = excluded.backward_verified_key_id",
(
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verifier.as_deref().unwrap_or(""),
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
self.secondary_verified_key_fingerprint
.as_ref()
.map(|fp| fp.hex()),
self.secondary_verifier.as_deref().unwrap_or(""),
self.backward_verified_key_id,
&self.addr,
),
)?;
Ok(())
};
sql.transaction(trans_fn).await
sql.execute(
"INSERT INTO acpeerstates (
last_seen,
last_seen_autocrypt,
prefer_encrypted,
public_key,
gossip_timestamp,
gossip_key,
public_key_fingerprint,
gossip_key_fingerprint,
verified_key,
verified_key_fingerprint,
verifier,
secondary_verified_key,
secondary_verified_key_fingerprint,
secondary_verifier,
backward_verified_key_id,
addr)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT (addr)
DO UPDATE SET
last_seen = excluded.last_seen,
last_seen_autocrypt = excluded.last_seen_autocrypt,
prefer_encrypted = excluded.prefer_encrypted,
public_key = excluded.public_key,
gossip_timestamp = excluded.gossip_timestamp,
gossip_key = excluded.gossip_key,
public_key_fingerprint = excluded.public_key_fingerprint,
gossip_key_fingerprint = excluded.gossip_key_fingerprint,
verified_key = excluded.verified_key,
verified_key_fingerprint = excluded.verified_key_fingerprint,
verifier = excluded.verifier,
secondary_verified_key = excluded.secondary_verified_key,
secondary_verified_key_fingerprint = excluded.secondary_verified_key_fingerprint,
secondary_verifier = excluded.secondary_verifier,
backward_verified_key_id = excluded.backward_verified_key_id",
(
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verifier.as_deref().unwrap_or(""),
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
self.secondary_verified_key_fingerprint
.as_ref()
.map(|fp| fp.hex()),
self.secondary_verifier.as_deref().unwrap_or(""),
self.backward_verified_key_id,
&self.addr,
),
)
.await?;
Ok(())
}
/// Returns the address that verified the contact
@@ -759,16 +732,14 @@ pub(crate) async fn maybe_do_aeap_transition(
// some accidental transitions if someone writes from multiple
// addresses with an MUA.
&& mime_parser.has_chat_version()
// Check if the message is encrypted and signed correctly. If it's not encrypted, it's
// probably from a new contact sharing the same key.
// Check if the message is signed correctly.
// Although checking `from_is_signed` below is sufficient, let's play it safe.
&& !mime_parser.signatures.is_empty()
// Check if the From: address was also in the signed part of the email.
// 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.
&& mime_parser.from_is_signed
// DC avoids sending messages with the same timestamp, that's why `>` is here unlike in
// `Peerstate::apply_header()`.
&& info.message_time > peerstate.last_seen
{
let info = &mut mime_parser.decryption_info;
@@ -783,16 +754,13 @@ pub(crate) async fn maybe_do_aeap_transition(
)
.await?;
let old_addr = mem::take(&mut peerstate.addr);
peerstate.addr = info.from.clone();
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(header, info.message_time);
peerstate
.save_to_db_ex(&context.sql, Some(&old_addr))
.await?;
peerstate.save_to_db(&context.sql).await?;
}
Ok(())
@@ -811,6 +779,30 @@ enum PeerstateChange {
Aeap(String),
}
/// Removes duplicate peerstates from `acpeerstates` database table.
///
/// Normally there should be no more than one peerstate per address.
/// However, the database does not enforce this condition.
///
/// Previously there were bugs that caused creation of additional
/// peerstates when existing peerstate could not be read due to a
/// temporary database error or a failure to parse stored data. This
/// procedure fixes the problem by removing duplicate records.
pub(crate) async fn deduplicate_peerstates(sql: &Sql) -> Result<()> {
sql.execute(
"DELETE FROM acpeerstates
WHERE id NOT IN (
SELECT MIN(id)
FROM acpeerstates
GROUP BY addr
)",
(),
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
+14 -24
View File
@@ -236,7 +236,6 @@ pub async fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: Vec<SignedPublicKey>,
private_key_for_signing: Option<SignedSecretKey>,
compress: bool,
) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
@@ -250,19 +249,20 @@ pub async fn pk_encrypt(
let mut rng = thread_rng();
// TODO: measure time
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
let signed_msg = lit_msg.sign(skey, || "".into(), HASH_ALGORITHM)?;
let compressed_msg = if compress {
signed_msg.compress(CompressionAlgorithm::ZLIB)?
} else {
signed_msg
};
compressed_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
lit_msg
.sign(skey, || "".into(), HASH_ALGORITHM)
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
.and_then(|msg| {
msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
})
} else {
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
};
let encoded_msg = encrypted_msg.to_armored_string(None)?;
let msg = encrypted_msg?;
let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg)
})
@@ -484,16 +484,10 @@ mod tests {
CTEXT_SIGNED
.get_or_init(|| async {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let compress = true;
pk_encrypt(
CLEARTEXT,
keyring,
Some(KEYS.alice_secret.clone()),
compress,
)
.await
.unwrap()
pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))
.await
.unwrap()
})
.await
}
@@ -503,11 +497,7 @@ mod tests {
CTEXT_UNSIGNED
.get_or_init(|| async {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let compress = true;
pk_encrypt(CLEARTEXT, keyring, None, compress)
.await
.unwrap()
pk_encrypt(CLEARTEXT, keyring, None).await.unwrap()
})
.await
}
+20 -38
View File
@@ -23,7 +23,6 @@ use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::socks::Socks5Config;
use crate::token;
use crate::tools::validate_id;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
@@ -250,6 +249,8 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
/// The function should be called after a QR code is scanned.
/// The function takes the raw text scanned and checks what can be done with it.
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
info!(context, "Scanned QR code: {}", qr);
let qrcode = if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
.await
@@ -346,18 +347,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
"".to_string()
};
let invitenumber = param
.get("i")
.filter(|&s| validate_id(s))
.map(|s| s.to_string());
let authcode = param
.get("s")
.filter(|&s| validate_id(s))
.map(|s| s.to_string());
let grpid = param
.get("x")
.filter(|&s| validate_id(s))
.map(|s| s.to_string());
let invitenumber = param.get("i").map(|s| s.to_string());
let authcode = param.get("s").map(|s| s.to_string());
let grpid = param.get("x").map(|s| s.to_string());
let grpname = if grpid.is_some() {
if let Some(encoded_name) = param.get("g") {
@@ -482,7 +474,8 @@ fn decode_account(qr: &str) -> Result<Qr> {
let payload = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("invalid DCACCOUNT payload")?;
let url = url::Url::parse(payload).context("Invalid account URL")?;
let url =
url::Url::parse(payload).with_context(|| format!("Invalid account URL: {payload:?}"))?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::Account {
domain: url
@@ -491,7 +484,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
.to_string(),
})
} else {
bail!("Bad scheme for account URL: {:?}.", url.scheme());
bail!("Bad scheme for account URL: {:?}.", payload);
}
}
@@ -502,7 +495,8 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
.context("invalid DCWEBRTC payload")?;
let (_type, url) = Message::parse_webrtc_instance(payload);
let url = url::Url::parse(&url).context("Invalid WebRTC instance")?;
let url =
url::Url::parse(&url).with_context(|| format!("Invalid WebRTC instance: {payload:?}"))?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::WebrtcInstance {
@@ -513,7 +507,7 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
instance_pattern: payload.to_string(),
})
} else {
bail!("Bad URL scheme for WebRTC instance: {:?}", url.scheme());
bail!("Bad URL scheme for WebRTC instance: {:?}", payload);
}
}
@@ -555,15 +549,16 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
.send()
.await?;
let response_status = response.status();
let response_text = response
.text()
.await
.context("Cannot create account, request failed: empty response")?;
let response_text = response.text().await.with_context(|| {
format!("Cannot create account, request to {url_str:?} failed: empty response")
})?;
if response_status.is_success() {
let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
.with_context(|| {
format!("Cannot create account, response is malformed:\n{response_text:?}")
format!(
"Cannot create account, response from {url_str:?} is malformed:\n{response_text:?}"
)
})?;
context
.set_config_internal(Config::Addr, Some(&email))
@@ -658,7 +653,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
Qr::Login { address, options } => {
configure_from_login_qr(context, &address, options).await?
}
_ => bail!("QR code does not contain config"),
_ => bail!("qr code {:?} does not contain config", qr),
}
Ok(())
@@ -821,6 +816,8 @@ fn normalize_address(addr: &str) -> Result<String> {
#[cfg(test)]
mod tests {
use anyhow::Result;
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus};
@@ -1043,21 +1040,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_invalid_token() -> Result<()> {
let ctx = TestContext::new().await;
// Token cannot contain "/"
let qr = check_qr(
&ctx.ctx,
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL/cxRL"
).await?;
assert!(matches!(qr, Qr::FprMismatch { .. }));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_openpgp_secure_join() -> Result<()> {
let ctx = TestContext::new().await;
+1 -1
View File
@@ -268,7 +268,7 @@ pub(crate) async fn configure_from_login_qr(
#[cfg(test)]
mod test {
use anyhow::bail;
use anyhow::{self, bail};
use super::{decode_login, LoginOptions};
use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr};
+4
View File
@@ -155,6 +155,10 @@ impl Context {
#[cfg(test)]
mod tests {
use super::*;
use crate::quota::{
QUOTA_ALLCLEAR_PERCENTAGE, QUOTA_ERROR_THRESHOLD_PERCENTAGE,
QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_needs_quota_warning() -> Result<()> {
+5 -241
View File
@@ -20,12 +20,11 @@ use std::fmt;
use anyhow::Result;
use crate::chat::{send_msg, Chat, ChatId};
use crate::chat::{send_msg, ChatId};
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
use crate::param::Param;
/// A single reaction consisting of multiple emoji sequences.
///
@@ -171,7 +170,6 @@ async fn set_msg_id_reaction(
msg_id: MsgId,
chat_id: ChatId,
contact_id: ContactId,
timestamp: i64,
reaction: Reaction,
) -> Result<()> {
if reaction.is_empty() {
@@ -196,17 +194,6 @@ async fn set_msg_id_reaction(
(msg_id, contact_id, reaction.as_str()),
)
.await?;
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat
.param
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
{
chat.param
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
chat.param
.set_i64(Param::LastReactionContactId, i64::from(contact_id.to_u32()));
chat.update_param(context).await?;
}
}
context.emit_event(EventType::ReactionsChanged {
@@ -236,15 +223,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
// Only set reaction if we successfully sent the message.
set_msg_id_reaction(
context,
msg_id,
msg.chat_id,
ContactId::SELF,
reaction_msg.timestamp_sort,
reaction,
)
.await?;
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
Ok(reaction_msg_id)
}
@@ -271,11 +250,10 @@ pub(crate) async fn set_msg_reaction(
in_reply_to: &str,
chat_id: ChatId,
contact_id: ContactId,
timestamp: i64,
reaction: Reaction,
) -> Result<()> {
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, reaction).await
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
} else {
info!(
context,
@@ -329,72 +307,18 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<React
Ok(Reactions { reactions })
}
impl Chat {
/// Check if there is a reaction newer than the given timestamp.
///
/// If so, reaction details are returned and can be used to create a summary string.
pub async fn get_last_reaction_if_newer_than(
&self,
context: &Context,
timestamp: i64,
) -> Result<Option<(Message, ContactId, String)>> {
if let Some(reaction_timestamp) = self.param.get_i64(Param::LastReactionTimestamp) {
if reaction_timestamp > timestamp {
let reaction_msg_id = MsgId::new(
self.param
.get_int(Param::LastReactionMsgId)
.unwrap_or_default() as u32,
);
// The message reacted to may be deleted physically (`load_from_db()` fails) or marked as a tombstone (`is_trash()`).
// These are no errors as `Param::LastReaction*` are just weak pointers.
// Instead, just return `Ok(None)` and let the caller create another summary.
if let Some(reaction_msg) =
Message::load_from_db_optional(context, reaction_msg_id).await?
{
if !reaction_msg.chat_id.is_trash() {
let reaction_contact_id = ContactId::new(
self.param
.get_int(Param::LastReactionContactId)
.unwrap_or_default() as u32,
);
if let Some(reaction) = context
.sql
.query_row_optional(
r#"SELECT reaction FROM reactions WHERE msg_id=? AND contact_id=?"#,
(reaction_msg.id, reaction_contact_id),
|row| {
let reaction: String = row.get(0)?;
Ok(reaction)
},
)
.await?
{
return Ok(Some((reaction_msg, reaction_contact_id, reaction)));
}
}
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{forward_msgs, get_chat_msgs, send_text_msg};
use crate::chatlist::Chatlist;
use crate::chat::get_chat_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::contact::{Contact, ContactAddress, Origin};
use crate::download::DownloadState;
use crate::message::{delete_msgs, MessageState};
use crate::message::MessageState;
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::sql::housekeeping;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools::SystemTime;
use std::time::Duration;
#[test]
fn test_parse_reaction() {
@@ -625,146 +549,6 @@ Here's my footer -- bob@example.net"
Ok(())
}
async fn assert_summary(t: &TestContext, expected: &str) {
let chatlist = Chatlist::try_load(t, 0, None, None).await.unwrap();
let summary = chatlist.get_summary(t, 0, None).await.unwrap();
assert_eq!(summary.text, expected);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_summary() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice.set_config(Config::Displayname, Some("ALICE")).await?;
bob.set_config(Config::Displayname, Some("BOB")).await?;
// Alice sends message to Bob
let alice_chat = alice.create_chat(&bob).await;
let alice_msg1 = alice.send_text(alice_chat.id, "Party?").await;
let bob_msg1 = bob.recv_msg(&alice_msg1).await;
// Bob reacts to Alice's message, this is shown in the summaries
SystemTime::shift(Duration::from_secs(10));
bob_msg1.chat_id.accept(&bob).await?;
send_reaction(&bob, bob_msg1.id, "👍").await?;
let bob_send_reaction = bob.pop_sent_msg().await;
let alice_rcvd_reaction = alice.recv_msg(&bob_send_reaction).await;
assert!(alice_rcvd_reaction.get_timestamp() > bob_msg1.get_timestamp());
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
let summary = chatlist.get_summary(&bob, 0, None).await?;
assert_eq!(summary.text, "You reacted 👍 to \"Party?\"");
assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); // time refers to message, not to reaction
assert_eq!(summary.state, MessageState::InFresh); // state refers to message, not to reaction
assert!(summary.prefix.is_none());
assert!(summary.thumbnail_path.is_none());
assert_summary(&alice, "BOB reacted 👍 to \"Party?\"").await;
// Alice reacts to own message as well
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?;
let alice_send_reaction = alice.pop_sent_msg().await;
bob.recv_msg(&alice_send_reaction).await;
assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await;
assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await;
// Alice sends a newer message, this overwrites reaction summaries
SystemTime::shift(Duration::from_secs(10));
let alice_msg2 = alice.send_text(alice_chat.id, "kewl").await;
bob.recv_msg(&alice_msg2).await;
assert_summary(&alice, "kewl").await;
assert_summary(&bob, "kewl").await;
// Reactions to older messages still overwrite newer messages
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "🤘").await?;
let alice_send_reaction = alice.pop_sent_msg().await;
bob.recv_msg(&alice_send_reaction).await;
assert_summary(&alice, "You reacted 🤘 to \"Party?\"").await;
assert_summary(&bob, "ALICE reacted 🤘 to \"Party?\"").await;
// Retracted reactions remove all summary reactions
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "").await?;
let alice_remove_reaction = alice.pop_sent_msg().await;
bob.recv_msg(&alice_remove_reaction).await;
assert_summary(&alice, "kewl").await;
assert_summary(&bob, "kewl").await;
// Alice adds another reaction and then deletes the message reacted to; this will also delete reaction summary
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, alice_msg1.sender_msg_id, "🧹").await?;
assert_summary(&alice, "You reacted 🧹 to \"Party?\"").await;
delete_msgs(&alice, &[alice_msg1.sender_msg_id]).await?; // this will leave a tombstone
assert_summary(&alice, "kewl").await;
housekeeping(&alice).await?; // this will delete the tombstone
assert_summary(&alice, "kewl").await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_forwarded_summary() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice adds a message to "Saved Messages"
let self_chat = alice.get_self_chat().await;
let msg_id = send_text_msg(&alice, self_chat.id, "foo".to_string()).await?;
assert_summary(&alice, "foo").await;
// Alice reacts to that message
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, msg_id, "🐫").await?;
assert_summary(&alice, "You reacted 🐫 to \"foo\"").await;
let reactions = get_msg_reactions(&alice, msg_id).await?;
assert_eq!(reactions.reactions.len(), 1);
// Alice forwards that message to Bob: Reactions are not forwarded, the message is prefixed by "Forwarded".
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let bob_chat_id = ChatId::create_for_contact(&alice, bob_id).await?;
forward_msgs(&alice, &[msg_id], bob_chat_id).await?;
assert_summary(&alice, "Forwarded: foo").await; // forwarded messages are prefixed
let chatlist = Chatlist::try_load(&alice, 0, None, None).await.unwrap();
let forwarded_msg_id = chatlist.get_msg_id(0)?.unwrap();
let reactions = get_msg_reactions(&alice, forwarded_msg_id).await?;
assert!(reactions.reactions.is_empty()); // reactions are not forwarded
// Alice reacts to forwarded message:
// For reaction summary neither original message author nor "Forwarded" prefix is shown
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice, forwarded_msg_id, "🐳").await?;
assert_summary(&alice, "You reacted 🐳 to \"foo\"").await;
let reactions = get_msg_reactions(&alice, msg_id).await?;
assert_eq!(reactions.reactions.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_self_chat_multidevice_summary() -> Result<()> {
let alice0 = TestContext::new_alice().await;
let alice1 = TestContext::new_alice().await;
let chat = alice0.get_self_chat().await;
let msg_id = send_text_msg(&alice0, chat.id, "mom's birthday!".to_string()).await?;
alice1.recv_msg(&alice0.pop_sent_msg().await).await;
SystemTime::shift(Duration::from_secs(10));
send_reaction(&alice0, msg_id, "👆").await?;
let sync = alice0.pop_sent_msg().await;
receive_imf(&alice1, sync.payload().as_bytes(), false).await?;
assert_summary(&alice0, "You reacted 👆 to \"mom's birthday!\"").await;
assert_summary(&alice1, "You reacted 👆 to \"mom's birthday!\"").await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_and_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -869,24 +653,4 @@ Here's my footer -- bob@example.net"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction_multidevice() -> Result<()> {
let alice0 = TestContext::new_alice().await;
let alice1 = TestContext::new_alice().await;
let bob_id = Contact::create(&alice0, "", "bob@example.net").await?;
let chat_id = ChatId::create_for_contact(&alice0, bob_id).await?;
let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
send_reaction(&alice0, alice0_msg_id, "👀").await?;
alice1.recv_msg(&alice0.pop_sent_msg().await).await;
expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
.await?;
Ok(())
}
}
+10 -20
View File
@@ -1,6 +1,7 @@
//! Internet Message Format reception pipeline.
use std::collections::HashSet;
use std::convert::TryFrom;
use anyhow::{Context as _, Result};
use mailparse::{parse_mail, SingleInfo};
@@ -37,9 +38,7 @@ use crate::simplify;
use crate::sql;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{
self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters, validate_id,
};
use crate::tools::{self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters};
use crate::{contact, imap};
/// This is the struct that is returned after receiving one email (aka MIME message).
@@ -316,9 +315,6 @@ pub(crate) async fn receive_imf_inner(
//
// If this is a mailing list email (i.e. list_id_header is some), don't change the displayname because in
// a mailing list the sender displayname sometimes does not belong to the sender email address.
// For example, GitHub sends messages from `notifications@github.com`,
// but uses display name of the user whose action generated the notification
// as the display name.
let (from_id, _from_id_blocked, incoming_origin) =
match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? {
Some(contact_id_res) => contact_id_res,
@@ -631,9 +627,12 @@ pub async fn from_field_to_contact_id(
if from_id == ContactId::SELF {
Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
} else {
let contact = Contact::get_by_id(context, from_id).await?;
let from_id_blocked = contact.blocked;
let incoming_origin = contact.origin;
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
if let Ok(contact) = Contact::get_by_id(context, from_id).await {
from_id_blocked = contact.blocked;
incoming_origin = contact.origin;
}
Ok(Some((from_id, from_id_blocked, incoming_origin)))
}
}
@@ -1077,12 +1076,6 @@ async fn add_parts(
chat_id_blocked = chat.blocked;
}
}
if chat_id.is_none() && is_dc_message == MessengerMessage::Yes {
if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
// automatically unblock chat when the user sends a message
if chat_id_blocked != Blocked::Not {
@@ -1380,7 +1373,6 @@ async fn add_parts(
&mime_in_reply_to,
orig_chat_id.unwrap_or_default(),
from_id,
sort_timestamp,
Reaction::from(reaction_str.as_str()),
)
.await?;
@@ -2364,10 +2356,7 @@ async fn apply_mailinglist_changes(
}
fn try_getting_grpid(mime_parser: &MimeMessage) -> Option<String> {
if let Some(optional_field) = mime_parser
.get_header(HeaderDef::ChatGroupId)
.filter(|s| validate_id(s))
{
if let Some(optional_field) = mime_parser.get_header(HeaderDef::ChatGroupId) {
return Some(optional_field.clone());
}
@@ -2690,6 +2679,7 @@ async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Opt
///
/// If none found, tries In-Reply-To: as a fallback for classic MUAs that don't set the
/// References: header.
// TODO also save first entry of References and look for this?
async fn get_parent_message(
context: &Context,
mime_parser: &MimeMessage,
+13 -245
View File
@@ -1,20 +1,19 @@
use std::time::Duration;
use tokio::fs;
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::{self, get_chat_msgs, ChatItem, ChatVisibility};
use crate::chat::{
add_contact_to_chat, add_to_chat_contacts_table, create_group_chat, get_chat_contacts,
get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg, ChatItem,
ChatVisibility,
is_contact_in_chat, remove_contact_from_chat, send_text_msg,
};
use crate::chatlist::Chatlist;
use crate::constants::{ShowEmails, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
use crate::download::MIN_DOWNLOAD_LIMIT;
use crate::config::Config;
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
use crate::download::{DownloadState, MIN_DOWNLOAD_LIMIT};
use crate::imap::prefetch_should_download;
use crate::imex::{imex, ImexMode};
use crate::message::{self, Message};
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
use crate::tools::SystemTime;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_grpid_simple() {
@@ -142,35 +141,6 @@ async fn test_adhoc_group_show_accepted_contact_unknown() {
assert_eq!(chats.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
bob.set_config(
Config::ShowEmails,
Some(&ShowEmails::AcceptedContacts.to_string()),
)
.await?;
tcm.send_recv(alice, bob, "hi").await;
receive_imf(
bob,
b"From: bob@example.net\n\
To: alice@example.org, claire@example.com\n\
Message-ID: <3333@example.net>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
false,
)
.await?;
let chats = Chatlist::try_load(bob, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0)?;
assert_eq!(chat_id.get_msg_cnt(bob).await?, 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_adhoc_group_show_accepted_contact_known() {
let t = TestContext::new_alice().await;
@@ -1722,7 +1692,7 @@ async fn test_in_reply_to_two_member_group() {
Subject: foo\n\
Message-ID: <message@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: foobarbaz12\n\
Chat-Group-ID: foo\n\
Chat-Group-Name: foo\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
@@ -1767,7 +1737,7 @@ async fn test_in_reply_to_two_member_group() {
Message-ID: <chatreply@example.org>\n\
In-Reply-To: <message@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: foobarbaz12\n\
Chat-Group-ID: foo\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
chat reply\n",
@@ -3576,32 +3546,6 @@ async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> {
Ok(())
}
/// If a message is Autocrypt-encrypted, unsigned Chat-Group-* headers have no effect.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unsigned_chat_group_hdr() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let bob_id = Contact::create(alice, "Bob", &bob_addr).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
let sent_msg = alice.pop_sent_msg().await;
let bob_chat_id = bob.recv_msg(&sent_msg).await.chat_id;
bob_chat_id.accept(bob).await?;
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
sent_msg.payload = sent_msg.payload.replace(
"Chat-Version:",
&format!("Chat-Group-Member-Removed: {bob_addr}\r\nChat-Version:"),
);
let chat_id = alice.recv_msg(&sent_msg).await.chat_id;
assert_eq!(chat_id, alice_chat_id);
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 2);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -3640,52 +3584,6 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
Ok(())
}
/// Test for the bug when remote group membership changes from outdated messages overrode local
/// ones. Especially that was a problem when a message is sent offline so that it doesn't
/// incorporate recent group membership changes.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_outdated_membership_changes() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_bob_id =
Contact::create(alice, "", &bob.get_config(Config::Addr).await?.unwrap()).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
// Alice creates a group chat. Bob accepts it.
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
let msg = &alice.pop_sent_msg().await;
bob.recv_msg(msg).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
bob_chat_id.accept(bob).await?;
// Bob replies.
send_text_msg(bob, bob_chat_id, "i'm bob".to_string()).await?;
let msg = &bob.pop_sent_msg().await;
SystemTime::shift(Duration::from_secs(3600));
// Alice leaves.
remove_contact_from_chat(alice, alice_chat_id, ContactId::SELF).await?;
alice.pop_sent_msg().await;
assert!(!is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
// Alice receives Bob's message, but it's outdated to add Alice back.
alice.recv_msg(msg).await;
assert!(!is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
SystemTime::shift(Duration::from_secs(3600));
// Bob replies again adding Alice back.
send_text_msg(bob, bob_chat_id, "i'm bob".to_string()).await?;
let msg = &bob.pop_sent_msg().await;
alice.recv_msg(msg).await;
assert!(is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -3969,6 +3867,7 @@ async fn test_mua_can_readd() -> Result<()> {
// And leaves it.
remove_contact_from_chat(&alice, alice_chat.id, ContactId::SELF).await?;
let alice_chat = Chat::load_from_db(&alice, alice_chat.id).await?;
assert!(!is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
// Bob uses a classical MUA to answer, adding Alice back.
@@ -3977,7 +3876,7 @@ async fn test_mua_can_readd() -> Result<()> {
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>, <claire@example.org>, <fiona@example.org>\r\n\
Date: Mon, 12 Dec 3000 14:32:39 +0000\r\n\
Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
\r\n\
@@ -3986,6 +3885,8 @@ async fn test_mua_can_readd() -> Result<()> {
)
.await?
.unwrap();
let alice_chat = Chat::load_from_db(&alice, alice_chat.id).await?;
assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
Ok(())
}
@@ -4115,37 +4016,6 @@ async fn test_download_later() -> Result<()> {
Ok(())
}
/// Malice can pretend they have the same address as Alice and sends a message encrypted to Alice's
/// key but signed with another one. Alice must detect that this message is wrongly signed and not
/// treat it as Autocrypt-encrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_msg_forgery() -> Result<()> {
let mut tcm = TestContextManager::new();
let export_dir = tempfile::tempdir().unwrap();
let alice = &tcm.alice().await;
let alice_addr = &alice.get_config(Config::Addr).await?.unwrap();
imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
// We need Bob only to encrypt the forged message to Alice's key, actually Bob doesn't
// participate in the scenario.
let bob = &TestContext::new().await;
bob.configure_addr("bob@example.net").await;
imex(bob, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
let malice = &TestContext::new().await;
malice.configure_addr(alice_addr).await;
let malice_chat_id = tcm
.send_recv_accept(bob, malice, "hi from bob")
.await
.chat_id;
let sent_msg = malice.send_text(malice_chat_id, "hi from malice").await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.state, MessageState::OutDelivered);
assert!(!msg.get_showpadlock());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_group_with_big_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -4339,105 +4209,3 @@ Chat-Group-Member-Added: charlie@example.com",
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forged_from() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id;
chat::send_text_msg(&bob, bob_chat_id, "hi!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
sent_msg.payload = sent_msg
.payload
.replace("bob@example.net", "notbob@example.net");
let msg = alice.recv_msg(&sent_msg).await;
assert!(msg.chat_id.is_trash());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_multiline_iso_8859_1_subject() -> Result<()> {
let t = &TestContext::new_alice().await;
let mail = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: bob@example.com\n\
To: alice@example.org, claire@example.com\n\
Subject:\n \
=?iso-8859-1?Q?Weihnachten_&_Silvester:_Feiern,_genie=DFen_&_entspannen_i?=\n \
=?iso-8859-1?Q?nmitten_der_Weing=E4rten?=\n\
Message-ID: <3333@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n";
receive_imf(t, mail, false).await?;
let msg = t.get_last_msg().await;
assert_eq!(
msg.get_subject(),
"Weihnachten & Silvester: Feiern, genießen & entspannen inmitten der Weingärten"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_references() -> 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 = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let _sent = alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
let bob_received_msg = bob.recv_msg(&sent).await;
let bob_chat_id = bob_received_msg.chat_id;
// Alice sends another three messages, but two of them are lost.
let _sent = alice.send_text(alice_chat_id, "Second message").await;
let _sent = alice.send_text(alice_chat_id, "Third message").await;
// Message can still be assigned based on the `References` header.
let sent = alice.send_text(alice_chat_id, "Fourth message").await;
let bob_parsed_message = bob.parse_msg(&sent).await;
let bob_parent_message = get_parent_message(bob, &bob_parsed_message).await?.unwrap();
assert_eq!(bob_chat_id, bob_parent_message.chat_id);
// If more messages are lost, message cannot be assigned to the correct chat
// without `Chat-Group-ID` header, e.g. if the message is partially downloaded.
let sent = alice.send_text(alice_chat_id, "Fifth message").await;
let bob_parsed_message = bob.parse_msg(&sent).await;
let bob_parent_message = get_parent_message(bob, &bob_parsed_message).await?;
assert!(bob_parent_message.is_none());
// When the message is received, it is assigned correctly because of `Chat-Group-ID` header.
let bob_received_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_chat_id, bob_received_msg.chat_id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_list_from() -> Result<()> {
let t = &TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/list-from.eml");
let received = receive_imf(t, raw, false).await?.unwrap();
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
assert_eq!(msg.get_override_sender_name().unwrap(), "ÖAMTC");
let sender_contact = Contact::get_by_id(t, msg.from_id).await?;
assert_eq!(
sender_contact.get_display_name(),
"clubinfo@donotreply.oeamtc.at"
);
let info = msg.id.get_info(t).await?;
assert!(info.contains(" by ~ÖAMTC (clubinfo@donotreply.oeamtc.at)"));
Ok(())
}
+4 -1
View File
@@ -830,7 +830,10 @@ impl Scheduler {
for (meaning, should_watch) in [
(FolderMeaning::Mvbox, ctx.should_watch_mvbox().await),
(FolderMeaning::Sent, ctx.should_watch_sentbox().await),
(
FolderMeaning::Sent,
ctx.get_config_bool(Config::SentboxWatch).await,
),
] {
if should_watch? {
let (conn_state, handlers) = ImapConnectionState::new(ctx).await?;
+1 -1
View File
@@ -170,7 +170,7 @@ impl ConnectivityStore {
}
/// Set all folder states to InterruptingIdle in case they were `Connected` before.
/// Called during `dc_maybe_network()` to make sure that `dc_all_work_done()`
/// Called during `dc_maybe_network()` to make sure that `dc_accounts_all_work_done()`
/// returns false immediately after `dc_maybe_network()`.
pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
let mut connectivity_lock = inbox.0.lock().await;
+60 -147
View File
@@ -1,5 +1,7 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://securejoin.readthedocs.io/en/latest/new.html#setup-contact-protocol).
use std::convert::TryFrom;
use anyhow::{bail, Context as _, Error, Result};
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
@@ -122,7 +124,8 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
)
};
info!(context, "Generated QR code.");
info!(context, "Generated QR code: {}", qr);
Ok(qr)
}
@@ -293,23 +296,6 @@ pub(crate) async fn handle_securejoin_handshake(
let join_vg = step.starts_with("vg-");
if !matches!(step.as_str(), "vg-request" | "vc-request") {
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.fingerprint();
for (addr, key) in &mime_message.gossiped_keys {
if key.fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
self_found = true;
break;
}
}
if !self_found {
// This message isn't intended for us. Possibly the peer doesn't own the key which the
// message is signed with but forwarded someone's message to us.
warn!(context, "Step {step}: No self addr+pubkey gossip found.");
return Ok(HandshakeMessage::Ignore);
}
}
match step.as_str() {
"vg-request" | "vc-request" => {
/*=======================================================
@@ -412,7 +398,7 @@ pub(crate) async fn handle_securejoin_handshake(
.await?;
return Ok(HandshakeMessage::Ignore);
};
let Some(group_chat_id) = token::auth_chat_id(context, auth).await? else {
if !token::exists(context, token::Namespace::Auth, auth).await? {
could_not_establish_secure_connection(
context,
contact_id,
@@ -421,8 +407,7 @@ pub(crate) async fn handle_securejoin_handshake(
)
.await?;
return Ok(HandshakeMessage::Ignore);
};
}
let contact_addr = Contact::get_by_id(context, contact_id)
.await?
.get_addr()
@@ -450,8 +435,41 @@ pub(crate) async fn handle_securejoin_handshake(
info!(context, "Auth verified.",);
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
inviter_progress(context, contact_id, 600);
if group_chat_id.is_unset() {
// Setup verified contact.
if join_vg {
// the vg-member-added message is special:
// this is a normal Chat-Group-Member-Added message
// with an additional Secure-Join header
let field_grpid = match mime_message.get_header(HeaderDef::SecureJoinGroup) {
Some(s) => s.as_str(),
None => {
warn!(context, "Missing Secure-Join-Group header");
return Ok(HandshakeMessage::Ignore);
}
};
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
Some((group_chat_id, _, _)) => {
secure_connection_established(
context,
contact_id,
group_chat_id,
mime_message.timestamp_sent,
)
.await?;
chat::add_contact_to_chat_ex(
context,
Nosync,
group_chat_id,
contact_id,
true,
)
.await?;
}
None => bail!("Chat {} not found", &field_grpid),
}
inviter_progress(context, contact_id, 800);
inviter_progress(context, contact_id, 1000);
} else {
// Alice -> Bob
secure_connection_established(
context,
contact_id,
@@ -464,19 +482,6 @@ pub(crate) async fn handle_securejoin_handshake(
.context("failed sending vc-contact-confirm message")?;
inviter_progress(context, contact_id, 1000);
} else {
// Join group.
secure_connection_established(
context,
contact_id,
group_chat_id,
mime_message.timestamp_sent,
)
.await?;
chat::add_contact_to_chat_ex(context, Nosync, group_chat_id, contact_id, true)
.await?;
inviter_progress(context, contact_id, 800);
inviter_progress(context, contact_id, 1000);
}
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
}
@@ -755,45 +760,32 @@ fn encrypted_and_signed(
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::remove_contact_from_chat;
use crate::chat;
use crate::chat::{remove_contact_from_chat, ProtectionStatus};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactAddress;
use crate::imex::{imex, ImexMode};
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str::chat_protection_enabled;
use crate::test_utils::get_chat_msg;
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::{EmailAddress, SystemTime};
use std::collections::HashSet;
use std::time::Duration;
#[derive(PartialEq)]
enum SetupContactCase {
Normal,
CheckProtectionTimestamp,
WrongAliceGossip,
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact() {
test_setup_contact_ex(SetupContactCase::Normal).await
test_setup_contact_ex(false).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_protection_timestamp() {
test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await
test_setup_contact_ex(true).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_wrong_alice_gossip() {
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
}
async fn test_setup_contact_ex(case: SetupContactCase) {
async fn test_setup_contact_ex(check_protection_timestamp: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
let bob = tcm.bob().await;
alice
.set_config(Config::VerifiedOneOnOneChats, Some("1"))
@@ -826,7 +818,10 @@ mod tests {
);
let sent = bob.pop_sent_msg().await;
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
assert_eq!(
sent.recipient(),
EmailAddress::new("alice@example.org").unwrap()
);
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
@@ -864,7 +859,7 @@ mod tests {
progress,
} => {
let alice_contact_id =
Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
@@ -876,7 +871,7 @@ mod tests {
// Check Bob sent the right message.
let sent = bob.pop_sent_msg().await;
let mut msg = alice.parse_msg(&sent).await;
let msg = alice.parse_msg(&sent).await;
let vc_request_with_auth_ts_sent = msg
.get_header(HeaderDef::Date)
.and_then(|value| mailparse::dateparse(value).ok())
@@ -893,30 +888,6 @@ mod tests {
bob_fp.hex()
);
if case == SetupContactCase::WrongAliceGossip {
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
let alice_pubkey = msg
.gossiped_keys
.insert(alice_addr.to_string(), wrong_pubkey)
.unwrap();
let contact_bob = alice.add_or_lookup_contact(&bob).await;
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
.await
.unwrap();
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
msg.gossiped_keys
.insert(alice_addr.to_string(), alice_pubkey)
.unwrap();
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
.await
.unwrap();
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
assert!(contact_bob.is_verified(&alice.ctx).await.unwrap());
return;
}
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
@@ -928,7 +899,7 @@ mod tests {
.unwrap();
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
if case == SetupContactCase::CheckProtectionTimestamp {
if check_protection_timestamp {
SystemTime::shift(Duration::from_secs(3600));
}
@@ -957,7 +928,7 @@ mod tests {
assert!(msg.is_info());
let expected_text = chat_protection_enabled(&alice).await;
assert_eq!(msg.get_text(), expected_text);
if case == SetupContactCase::CheckProtectionTimestamp {
if check_protection_timestamp {
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent);
}
}
@@ -972,10 +943,11 @@ mod tests {
);
// Bob should not yet have Alice verified
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
.await
.unwrap();
@@ -1173,14 +1145,6 @@ mod tests {
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
// but it was only used by Alice in `vg-request-with-auth`.
// New Delta Chat versions do not use `Secure-Join-Group` header at all
// and it is deprecated.
// Now `Secure-Join-Group` header
// is only sent in `vg-request-with-auth` for compatibility.
assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none());
// Step 3: Alice receives vg-request, sends vg-auth-required
alice.recv_msg(&sent).await;
@@ -1432,55 +1396,4 @@ First thread."#;
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true);
}
/// An unencrypted message with already known Autocrypt key, but sent from another address,
/// means that it's rather a new contact sharing the same key than the existing one changed its
/// address, otherwise it would already have our key to encrypt.
///
/// This is a regression test for a bug where DC wrongly executed AEAP in this case.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_shared_bobs_key() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
tcm.execute_securejoin(bob, alice).await;
let export_dir = tempfile::tempdir().unwrap();
imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
let bob2 = &TestContext::new().await;
let bob2_addr = "bob2@example.net";
bob2.configure_addr(bob2_addr).await;
imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
tcm.execute_securejoin(bob2, alice).await;
let bob3 = &TestContext::new().await;
let bob3_addr = "bob3@example.net";
bob3.configure_addr(bob3_addr).await;
imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
tcm.send_recv(bob3, alice, "hi Alice!").await;
let msg = tcm.send_recv(alice, bob3, "hi Bob3!").await;
assert!(msg.get_showpadlock());
let mut bob_ids = HashSet::new();
bob_ids.insert(
Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown)
.await?
.unwrap(),
);
bob_ids.insert(
Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown)
.await?
.unwrap(),
);
bob_ids.insert(
Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown)
.await?
.unwrap(),
);
assert_eq!(bob_ids.len(), 3);
Ok(())
}
}
+5 -12
View File
@@ -378,21 +378,14 @@ async fn send_handshake_message(
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
let bob_fp = load_self_public_key(context).await?.fingerprint();
msg.param.set(Param::Arg3, bob_fp.hex());
// Sends the grpid in the Secure-Join-Group header.
//
// `Secure-Join-Group` header is deprecated,
// but old Delta Chat core requires that Alice receives it.
//
// Previous Delta Chat core also sent `Secure-Join-Group` header
// in `vg-request` messages,
// but it was not used on the receiver.
if let QrInvite::Group { ref grpid, .. } = invite {
msg.param.set(Param::Arg4, grpid);
}
}
};
// Sends the grpid in the Secure-Join-Group header.
if let QrInvite::Group { ref grpid, .. } = invite {
msg.param.set(Param::Arg4, grpid);
}
chat::send_msg(context, chat_id, &mut msg).await?;
Ok(())
}
+3 -1
View File
@@ -4,6 +4,8 @@
//! with it rather hard, so here we have a wrapper type that specifically deals with Secure-Join
//! QR-codes so that the Secure-Join code can have more guarantees when dealing with this.
use std::convert::TryFrom;
use anyhow::{bail, Error, Result};
use crate::contact::ContactId;
@@ -95,7 +97,7 @@ impl TryFrom<Qr> for QrInvite {
invitenumber,
authcode,
}),
_ => bail!("Unsupported QR type"),
_ => bail!("Unsupported QR type {:?}", qr),
}
}
}
+20 -20
View File
@@ -1,10 +1,11 @@
//! # SQLite wrapper.
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context as _, Result};
use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row};
use rusqlite::{self, config::DbConfig, types::ValueRef, Connection, OpenFlags, Row};
use tokio::sync::{Mutex, MutexGuard, RwLock};
use crate::blob::BlobObject;
@@ -18,7 +19,7 @@ use crate::imex::BLOBS_BACKUP_NAME;
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::peerstate::{deduplicate_peerstates, Peerstate};
use crate::stock_str;
use crate::tools::{delete_file, time, SystemTime};
@@ -46,12 +47,10 @@ pub(crate) fn params_iter(
iter.iter().map(|item| item as &dyn crate::sql::ToSql)
}
mod deserialize;
mod migrations;
mod pool;
mod serialize;
use pool::{Pool, PooledConnection};
use pool::Pool;
/// A wrapper around the underlying Sqlite3 object.
#[derive(Debug)]
@@ -365,12 +364,6 @@ impl Sql {
self.write_mtx.lock().await
}
pub(crate) async fn get_connection(&self) -> Result<PooledConnection> {
let lock = self.pool.read().await;
let pool = lock.as_ref().context("no SQL connection")?;
pool.get().await
}
/// Allocates a connection and calls `function` with the connection. If `function` does write
/// queries,
/// - either first take a lock using `write_lock()`
@@ -382,7 +375,9 @@ impl Sql {
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
R: Send + 'static,
{
let mut conn = self.get_connection().await?;
let lock = self.pool.read().await;
let pool = lock.as_ref().context("no SQL connection")?;
let mut conn = pool.get().await?;
let res = tokio::task::block_in_place(move || function(&mut conn))?;
Ok(res)
}
@@ -683,18 +678,11 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
"PRAGMA cipher_memory_security = OFF; -- Too slow on Android
PRAGMA secure_delete=on;
PRAGMA busy_timeout = 0; -- fail immediately
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA soft_heap_limit = 8388608; -- 8 MiB limit, same as set in Android SQLiteDatabase.
PRAGMA foreign_keys=on;
",
)?;
// Avoid SQLITE_IOERR_GETTEMPPATH errors on Android and maybe other systems.
// Downside is more RAM consumption esp. on VACUUM.
// Therefore, on systems known to have working default (using files), stay with that.
if cfg!(not(target_os = "ios")) {
conn.pragma_update(None, "temp_store", "memory")?;
}
conn.pragma_update(None, "key", passphrase)?;
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
@@ -743,6 +731,10 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
);
}
if let Err(err) = deduplicate_peerstates(&context.sql).await {
warn!(context, "Failed to deduplicate peerstates: {:#}.", err)
}
// Try to clear the freelist to free some space on the disk. This
// only works if auto_vacuum is enabled.
match context
@@ -812,6 +804,13 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
Param::File,
)
.await?;
maybe_add_from_param(
&context.sql,
&mut files_in_use,
"SELECT param FROM jobs;",
Param::File,
)
.await?;
maybe_add_from_param(
&context.sql,
&mut files_in_use,
@@ -1016,6 +1015,7 @@ mod tests {
use async_channel as channel;
use super::*;
use crate::config::Config;
use crate::{test_utils::TestContext, EventType};
#[test]
File diff suppressed because it is too large Load Diff
-963
View File
@@ -1,963 +0,0 @@
//! Database serialization module.
//!
//! The module contains functions to serialize database into a stream.
//!
//! Output format is based on [bencoding](http://bittorrent.org/beps/bep_0003.html).
/// Database version supported by the current serialization code.
///
/// Serialization code MUST be updated before increasing this number.
///
/// If this version is below the actual database version,
/// serialization code is outdated.
/// If this version is above the actual database version,
/// migrations have to be run first to update the database.
const SERIALIZE_DBVERSION: &str = "99";
use anyhow::{anyhow, Context as _, Result};
use rusqlite::types::ValueRef;
use rusqlite::Transaction;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use super::Sql;
struct Encoder<'a, W: AsyncWrite + Unpin> {
tx: Transaction<'a>,
w: W,
}
async fn write_bytes(w: &mut (impl AsyncWrite + Unpin), b: &[u8]) -> Result<()> {
let bytes_len = format!("{}:", b.len());
w.write_all(bytes_len.as_bytes()).await?;
w.write_all(b).await?;
Ok(())
}
async fn write_str(w: &mut (impl AsyncWrite + Unpin), s: &str) -> Result<()> {
write_bytes(w, s.as_bytes()).await?;
Ok(())
}
async fn write_i64(w: &mut (impl AsyncWrite + Unpin), i: i64) -> Result<()> {
let s = format!("{i}");
w.write_all(b"i").await?;
w.write_all(s.as_bytes()).await?;
w.write_all(b"e").await?;
Ok(())
}
async fn write_u32(w: &mut (impl AsyncWrite + Unpin), i: u32) -> Result<()> {
let s = format!("{i}");
w.write_all(b"i").await?;
w.write_all(s.as_bytes()).await?;
w.write_all(b"e").await?;
Ok(())
}
async fn write_f64(w: &mut (impl AsyncWrite + Unpin), f: f64) -> Result<()> {
write_bytes(w, &f.to_be_bytes()).await?;
Ok(())
}
async fn write_bool(w: &mut (impl AsyncWrite + Unpin), b: bool) -> Result<()> {
if b {
w.write_all(b"i1e").await?;
} else {
w.write_all(b"i0e").await?;
}
Ok(())
}
impl<'a, W: AsyncWrite + Unpin> Encoder<'a, W> {
fn new(tx: Transaction<'a>, w: W) -> Self {
Self { tx, w }
}
/// Serializes `config` table.
async fn serialize_config(&mut self) -> Result<()> {
// FIXME: sort the dictionary in lexicographical order
// dbversion should be the first, so store it as "_config._dbversion"
let mut stmt = self.tx.prepare("SELECT keyname,value FROM config")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"d").await?;
while let Some(row) = rows.next()? {
let keyname: String = row.get(0)?;
let value: String = row.get(1)?;
write_str(&mut self.w, &keyname).await?;
write_str(&mut self.w, &value).await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_acpeerstates(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare("SELECT addr, backward_verified_key_id, last_seen, last_seen_autocrypt, public_key, prefer_encrypted, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let addr: String = row.get("addr")?;
let backward_verified_key_id: Option<i64> = row.get("backward_verified_key_id")?;
let prefer_encrypted: i64 = row.get("prefer_encrypted")?;
let last_seen: i64 = row.get("last_seen")?;
let last_seen_autocrypt: i64 = row.get("last_seen_autocrypt")?;
let public_key: Option<Vec<u8>> = row.get("public_key")?;
let public_key_fingerprint: Option<String> = row.get("public_key_fingerprint")?;
let gossip_timestamp: i64 = row.get("gossip_timestamp")?;
let gossip_key: Option<Vec<u8>> = row.get("gossip_key")?;
let gossip_key_fingerprint: Option<String> = row.get("gossip_key_fingerprint")?;
let verified_key: Option<Vec<u8>> = row.get("verified_key")?;
let verified_key_fingerprint: Option<String> = row.get("verified_key_fingerprint")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "addr").await?;
write_str(&mut self.w, &addr).await?;
if let Some(backward_verified_key_id) = backward_verified_key_id {
write_str(&mut self.w, "backward_verified_key_id").await?;
write_i64(&mut self.w, backward_verified_key_id).await?;
}
if let Some(gossip_key) = gossip_key {
write_str(&mut self.w, "gossip_key").await?;
write_bytes(&mut self.w, &gossip_key).await?;
}
if let Some(gossip_key_fingerprint) = gossip_key_fingerprint {
write_str(&mut self.w, "gossip_key_fingerprint").await?;
write_str(&mut self.w, &gossip_key_fingerprint).await?;
}
write_str(&mut self.w, "gossip_timestamp").await?;
write_i64(&mut self.w, gossip_timestamp).await?;
write_str(&mut self.w, "last_seen").await?;
write_i64(&mut self.w, last_seen).await?;
write_str(&mut self.w, "last_seen_autocrypt").await?;
write_i64(&mut self.w, last_seen_autocrypt).await?;
write_str(&mut self.w, "prefer_encrypted").await?;
write_i64(&mut self.w, prefer_encrypted).await?;
if let Some(public_key) = public_key {
write_str(&mut self.w, "public_key").await?;
write_bytes(&mut self.w, &public_key).await?;
}
if let Some(public_key_fingerprint) = public_key_fingerprint {
write_str(&mut self.w, "public_key_fingerprint").await?;
write_str(&mut self.w, &public_key_fingerprint).await?;
}
if let Some(verified_key) = verified_key {
write_str(&mut self.w, "verified_key").await?;
write_bytes(&mut self.w, &verified_key).await?;
}
if let Some(verified_key_fingerprint) = verified_key_fingerprint {
write_str(&mut self.w, "verified_key_fingerprint").await?;
write_str(&mut self.w, &verified_key_fingerprint).await?;
}
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes chats.
async fn serialize_chats(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare(
"SELECT \
id,\
type,\
name,\
blocked,\
grpid,\
param,\
archived,\
gossiped_timestamp,\
locations_send_begin,\
locations_send_until,\
locations_last_sent,\
created_timestamp,\
muted_until,\
ephemeral_timer,\
protected FROM chats",
)?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: u32 = row.get("id")?;
let typ: u32 = row.get("type")?;
let name: String = row.get("name")?;
let blocked: u32 = row.get("blocked")?;
let grpid: String = row.get("grpid")?;
let param: String = row.get("param")?;
let archived: bool = row.get("archived")?;
let gossiped_timestamp: i64 = row.get("gossiped_timestamp")?;
let locations_send_begin: i64 = row.get("locations_send_begin")?;
let locations_send_until: i64 = row.get("locations_send_until")?;
let locations_last_sent: i64 = row.get("locations_last_sent")?;
let created_timestamp: i64 = row.get("created_timestamp")?;
let muted_until: i64 = row.get("muted_until")?;
let ephemeral_timer: i64 = row.get("ephemeral_timer")?;
let protected: u32 = row.get("protected")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "archived").await?;
write_bool(&mut self.w, archived).await?;
write_str(&mut self.w, "blocked").await?;
write_u32(&mut self.w, blocked).await?;
write_str(&mut self.w, "created_timestamp").await?;
write_i64(&mut self.w, created_timestamp).await?;
write_str(&mut self.w, "ephemeral_timer").await?;
write_i64(&mut self.w, ephemeral_timer).await?;
write_str(&mut self.w, "gossiped_timestamp").await?;
write_i64(&mut self.w, gossiped_timestamp).await?;
write_str(&mut self.w, "grpid").await?;
write_str(&mut self.w, &grpid).await?;
write_str(&mut self.w, "id").await?;
write_u32(&mut self.w, id).await?;
write_str(&mut self.w, "locations_last_sent").await?;
write_i64(&mut self.w, locations_last_sent).await?;
write_str(&mut self.w, "locations_send_begin").await?;
write_i64(&mut self.w, locations_send_begin).await?;
write_str(&mut self.w, "locations_send_until").await?;
write_i64(&mut self.w, locations_send_until).await?;
write_str(&mut self.w, "muted_until").await?;
write_i64(&mut self.w, muted_until).await?;
write_str(&mut self.w, "name").await?;
write_str(&mut self.w, &name).await?;
write_str(&mut self.w, "param").await?;
write_str(&mut self.w, &param).await?;
write_str(&mut self.w, "protected").await?;
write_u32(&mut self.w, protected).await?;
write_str(&mut self.w, "type").await?;
write_u32(&mut self.w, typ).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_chats_contacts(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT chat_id, contact_id FROM chats_contacts")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let chat_id: u32 = row.get("chat_id")?;
let contact_id: u32 = row.get("contact_id")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "chat_id").await?;
write_u32(&mut self.w, chat_id).await?;
write_str(&mut self.w, "contact_id").await?;
write_u32(&mut self.w, contact_id).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes contacts.
async fn serialize_contacts(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare(
"SELECT \
id,\
name,\
addr,\
origin,\
blocked,\
last_seen,\
param,\
authname,\
selfavatar_sent,\
status FROM contacts",
)?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: u32 = row.get("id")?;
let name: String = row.get("name")?;
let authname: String = row.get("authname")?;
let addr: String = row.get("addr")?;
let origin: u32 = row.get("origin")?;
let blocked: Option<bool> = row.get("blocked")?;
let blocked = blocked.unwrap_or_default();
let last_seen: i64 = row.get("last_seen")?;
let selfavatar_sent: i64 = row.get("selfavatar_sent")?;
let param: String = row.get("param")?;
let status: Option<String> = row.get("status")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "addr").await?;
write_str(&mut self.w, &addr).await?;
write_str(&mut self.w, "authname").await?;
write_str(&mut self.w, &authname).await?;
write_str(&mut self.w, "blocked").await?;
write_bool(&mut self.w, blocked).await?;
write_str(&mut self.w, "id").await?;
write_u32(&mut self.w, id).await?;
write_str(&mut self.w, "last_seen").await?;
write_i64(&mut self.w, last_seen).await?;
write_str(&mut self.w, "name").await?;
write_str(&mut self.w, &name).await?;
write_str(&mut self.w, "origin").await?;
write_u32(&mut self.w, origin).await?;
// TODO: parse param instead of serializeing as is
write_str(&mut self.w, "param").await?;
write_str(&mut self.w, &param).await?;
write_str(&mut self.w, "selfavatar_sent").await?;
write_i64(&mut self.w, selfavatar_sent).await?;
if let Some(status) = status {
if !status.is_empty() {
write_str(&mut self.w, "status").await?;
write_str(&mut self.w, &status).await?;
}
}
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_dns_cache(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT hostname, address, timestamp FROM dns_cache")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let hostname: String = row.get("hostname")?;
let address: String = row.get("address")?;
let timestamp: i64 = row.get("timestamp")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "address").await?;
write_str(&mut self.w, &address).await?;
write_str(&mut self.w, "hostname").await?;
write_str(&mut self.w, &hostname).await?;
write_str(&mut self.w, "timestamp").await?;
write_i64(&mut self.w, timestamp).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_imap(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id, rfc724_mid, folder, target, uid, uidvalidity FROM imap")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let folder: String = row.get("folder")?;
let target: String = row.get("target")?;
let uid: i64 = row.get("uid")?;
let uidvalidity: i64 = row.get("uidvalidity")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "folder").await?;
write_str(&mut self.w, &folder).await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "rfc724_mid").await?;
write_str(&mut self.w, &rfc724_mid).await?;
write_str(&mut self.w, "target").await?;
write_str(&mut self.w, &target).await?;
write_str(&mut self.w, "uid").await?;
write_i64(&mut self.w, uid).await?;
write_str(&mut self.w, "uidvalidity").await?;
write_i64(&mut self.w, uidvalidity).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_imap_sync(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT folder, uidvalidity, uid_next, modseq FROM imap_sync")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let folder: String = row.get("folder")?;
let uidvalidity: i64 = row.get("uidvalidity")?;
let uidnext: i64 = row.get("uid_next")?;
let modseq: i64 = row.get("modseq")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "folder").await?;
write_str(&mut self.w, &folder).await?;
write_str(&mut self.w, "modseq").await?;
write_i64(&mut self.w, modseq).await?;
write_str(&mut self.w, "uidnext").await?;
write_i64(&mut self.w, uidnext).await?;
write_str(&mut self.w, "uidvalidity").await?;
write_i64(&mut self.w, uidvalidity).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_keypairs(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id,addr,private_key,public_key,created FROM keypairs")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: u32 = row.get("id")?;
let addr: String = row.get("addr")?;
let private_key: Vec<u8> = row.get("private_key")?;
let public_key: Vec<u8> = row.get("public_key")?;
let created: i64 = row.get("created")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "addr").await?;
write_str(&mut self.w, &addr).await?;
write_str(&mut self.w, "created").await?;
write_i64(&mut self.w, created).await?;
write_str(&mut self.w, "id").await?;
write_u32(&mut self.w, id).await?;
write_str(&mut self.w, "private_key").await?;
write_bytes(&mut self.w, &private_key).await?;
write_str(&mut self.w, "public_key").await?;
write_bytes(&mut self.w, &public_key).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_leftgroups(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare("SELECT grpid FROM leftgrps")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let grpid: String = row.get("grpid")?;
write_str(&mut self.w, &grpid).await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_locations(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id, latitude, longitude, accuracy, timestamp, chat_id, from_id, independent FROM locations")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let latitude: f64 = row.get("latitude")?;
let longitude: f64 = row.get("longitude")?;
let accuracy: f64 = row.get("accuracy")?;
let timestamp: i64 = row.get("timestamp")?;
let chat_id: u32 = row.get("chat_id")?;
let from_id: u32 = row.get("from_id")?;
let independent: u32 = row.get("independent")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "accuracy").await?;
write_f64(&mut self.w, accuracy).await?;
write_str(&mut self.w, "chat_id").await?;
write_u32(&mut self.w, chat_id).await?;
write_str(&mut self.w, "from_id").await?;
write_u32(&mut self.w, from_id).await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "independent").await?;
write_u32(&mut self.w, independent).await?;
write_str(&mut self.w, "latitude").await?;
write_f64(&mut self.w, latitude).await?;
write_str(&mut self.w, "longitude").await?;
write_f64(&mut self.w, longitude).await?;
write_str(&mut self.w, "timestamp").await?;
write_i64(&mut self.w, timestamp).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes MDNs.
async fn serialize_mdns(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT msg_id, contact_id, timestamp_sent FROM msgs_mdns")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let msg_id: u32 = row.get("msg_id")?;
let contact_id: u32 = row.get("contact_id")?;
let timestamp_sent: i64 = row.get("timestamp_sent")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "contact_id").await?;
write_u32(&mut self.w, contact_id).await?;
write_str(&mut self.w, "msg_id").await?;
write_u32(&mut self.w, msg_id).await?;
write_str(&mut self.w, "timestamp_sent").await?;
write_i64(&mut self.w, timestamp_sent).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes messages.
async fn serialize_messages(&mut self) -> Result<()> {
let mut stmt = self.tx.prepare(
"SELECT
id,
rfc724_mid,
chat_id,
from_id, to_id,
timestamp,
type,
state,
msgrmsg,
bytes,
txt,
txt_raw,
param,
timestamp_sent,
timestamp_rcvd,
hidden,
mime_compressed,
mime_headers,
mime_in_reply_to,
mime_references,
location_id FROM msgs",
)?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let chat_id: i64 = row.get("chat_id")?;
let from_id: i64 = row.get("from_id")?;
let to_id: i64 = row.get("to_id")?;
let timestamp: i64 = row.get("timestamp")?;
let typ: i64 = row.get("type")?;
let state: i64 = row.get("state")?;
let msgrmsg: i64 = row.get("msgrmsg")?;
let bytes: i64 = row.get("bytes")?;
let txt: String = row.get("txt")?;
let txt_raw: String = row.get("txt_raw")?;
let param: String = row.get("param")?;
let timestamp_sent: i64 = row.get("timestamp_sent")?;
let timestamp_rcvd: i64 = row.get("timestamp_rcvd")?;
let hidden: i64 = row.get("hidden")?;
let mime_compressed: i64 = row.get("mime_compressed")?;
let mime_headers: Vec<u8> =
row.get("mime_headers")
.or_else(|err| match row.get_ref("mime_headers")? {
ValueRef::Null => Ok(Vec::new()),
ValueRef::Text(text) => Ok(text.to_vec()),
ValueRef::Blob(blob) => Ok(blob.to_vec()),
ValueRef::Integer(_) | ValueRef::Real(_) => Err(err),
})?;
let mime_in_reply_to: Option<String> = row.get("mime_in_reply_to")?;
let mime_references: Option<String> = row.get("mime_references")?;
let location_id: i64 = row.get("location_id")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "bytes").await?;
write_i64(&mut self.w, bytes).await?;
write_str(&mut self.w, "chat_id").await?;
write_i64(&mut self.w, chat_id).await?;
write_str(&mut self.w, "from_id").await?;
write_i64(&mut self.w, from_id).await?;
write_str(&mut self.w, "hidden").await?;
write_i64(&mut self.w, hidden).await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "location_id").await?;
write_i64(&mut self.w, location_id).await?;
write_str(&mut self.w, "mime_compressed").await?;
write_i64(&mut self.w, mime_compressed).await?;
write_str(&mut self.w, "mime_headers").await?;
write_bytes(&mut self.w, &mime_headers).await?;
if let Some(mime_in_reply_to) = mime_in_reply_to {
write_str(&mut self.w, "mime_in_reply_to").await?;
write_str(&mut self.w, &mime_in_reply_to).await?;
}
if let Some(mime_references) = mime_references {
write_str(&mut self.w, "mime_references").await?;
write_str(&mut self.w, &mime_references).await?;
}
write_str(&mut self.w, "msgrmsg").await?;
write_i64(&mut self.w, msgrmsg).await?;
write_str(&mut self.w, "param").await?;
write_str(&mut self.w, &param).await?;
write_str(&mut self.w, "rfc724_mid").await?;
write_str(&mut self.w, &rfc724_mid).await?;
write_str(&mut self.w, "state").await?;
write_i64(&mut self.w, state).await?;
write_str(&mut self.w, "timestamp").await?;
write_i64(&mut self.w, timestamp).await?;
write_str(&mut self.w, "timestamp_rcvd").await?;
write_i64(&mut self.w, timestamp_rcvd).await?;
write_str(&mut self.w, "timestamp_sent").await?;
write_i64(&mut self.w, timestamp_sent).await?;
write_str(&mut self.w, "to_id").await?;
write_i64(&mut self.w, to_id).await?;
write_str(&mut self.w, "txt").await?;
write_str(&mut self.w, &txt).await?;
write_str(&mut self.w, "txt_raw").await?;
write_str(&mut self.w, &txt_raw).await?;
write_str(&mut self.w, "type").await?;
write_i64(&mut self.w, typ).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_msgs_status_updates(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id, msg_id, uid, update_item FROM msgs_status_updates")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let msg_id: i64 = row.get("msg_id")?;
let uid: String = row.get("uid")?;
let update_item: String = row.get("update_item")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "msg_id").await?;
write_i64(&mut self.w, msg_id).await?;
write_str(&mut self.w, "uid").await?;
write_str(&mut self.w, &uid).await?;
write_str(&mut self.w, "update_item").await?;
write_str(&mut self.w, &update_item).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
/// Serializes reactions.
async fn serialize_reactions(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT msg_id, contact_id, reaction FROM reactions")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let msg_id: u32 = row.get("msg_id")?;
let contact_id: u32 = row.get("contact_id")?;
let reaction: String = row.get("reaction")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "contact_id").await?;
write_u32(&mut self.w, contact_id).await?;
write_str(&mut self.w, "msg_id").await?;
write_u32(&mut self.w, msg_id).await?;
write_str(&mut self.w, "reaction").await?;
write_str(&mut self.w, &reaction).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_sending_domains(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT domain, dkim_works FROM sending_domains")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let domain: String = row.get("domain")?;
let dkim_works: i64 = row.get("dkim_works")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "dkim_works").await?;
write_i64(&mut self.w, dkim_works).await?;
write_str(&mut self.w, "domain").await?;
write_str(&mut self.w, &domain).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize_tokens(&mut self) -> Result<()> {
let mut stmt = self
.tx
.prepare("SELECT id, namespc, foreign_id, token, timestamp FROM tokens")?;
let mut rows = stmt.query(())?;
self.w.write_all(b"l").await?;
while let Some(row) = rows.next()? {
let id: i64 = row.get("id")?;
let namespace: u32 = row.get("namespc")?;
let foreign_id: u32 = row.get("foreign_id")?;
let token: String = row.get("token")?;
let timestamp: i64 = row.get("timestamp")?;
self.w.write_all(b"d").await?;
write_str(&mut self.w, "foreign_id").await?;
write_u32(&mut self.w, foreign_id).await?;
write_str(&mut self.w, "id").await?;
write_i64(&mut self.w, id).await?;
write_str(&mut self.w, "namespace").await?;
write_u32(&mut self.w, namespace).await?;
write_str(&mut self.w, "timestamp").await?;
write_i64(&mut self.w, timestamp).await?;
write_str(&mut self.w, "token").await?;
write_str(&mut self.w, &token).await?;
self.w.write_all(b"e").await?;
}
self.w.write_all(b"e").await?;
Ok(())
}
async fn serialize(&mut self) -> Result<()> {
let dbversion: String = self.tx.query_row(
"SELECT value FROM config WHERE keyname='dbversion'",
(),
|row| row.get(0),
)?;
if dbversion != SERIALIZE_DBVERSION {
return Err(anyhow!(
"cannot serialize database version {dbversion}, expected {SERIALIZE_DBVERSION}"
));
}
self.w.write_all(b"d").await?;
write_str(&mut self.w, "_config").await?;
self.serialize_config().await?;
write_str(&mut self.w, "acpeerstates").await?;
self.serialize_acpeerstates()
.await
.context("serialize autocrypt peerstates")?;
write_str(&mut self.w, "chats").await?;
self.serialize_chats().await?;
write_str(&mut self.w, "chats_contacts").await?;
self.serialize_chats_contacts()
.await
.context("serialize chats_contacts")?;
write_str(&mut self.w, "contacts").await?;
self.serialize_contacts().await?;
write_str(&mut self.w, "dns_cache").await?;
self.serialize_dns_cache()
.await
.context("serialize dns_cache")?;
write_str(&mut self.w, "imap").await?;
self.serialize_imap().await.context("serialize imap")?;
write_str(&mut self.w, "imap_sync").await?;
self.serialize_imap_sync()
.await
.context("serialize imap_sync")?;
write_str(&mut self.w, "keypairs").await?;
self.serialize_keypairs().await?;
write_str(&mut self.w, "leftgroups").await?;
self.serialize_leftgroups().await?;
write_str(&mut self.w, "locations").await?;
self.serialize_locations().await?;
write_str(&mut self.w, "mdns").await?;
self.serialize_mdns().await?;
write_str(&mut self.w, "messages").await?;
self.serialize_messages()
.await
.context("serialize messages")?;
write_str(&mut self.w, "msgs_status_updates").await?;
self.serialize_msgs_status_updates()
.await
.context("serialize msgs_status_updates")?;
write_str(&mut self.w, "reactions").await?;
self.serialize_reactions().await?;
write_str(&mut self.w, "sending_domains").await?;
self.serialize_sending_domains()
.await
.context("serialize sending_domains")?;
write_str(&mut self.w, "tokens").await?;
self.serialize_tokens().await?;
// jobs table is skipped
// multi_device_sync is skipped
// imap_markseen is skipped, it is usually empty and the device exporting the
// database should still be able to clear it.
// smtp, smtp_mdns and smtp_status_updates tables are skipped, they are part of the
// outgoing message queue.
// devmsglabels is skipped, it is reset in `delete_and_reset_all_device_msgs()` on import
// anyway
// bobstate is not serialized, it is temporary for joining or adding a contact.
//
// TODO insert welcome message on import like done in `delete_and_reset_all_device_msgs()`?
self.w.write_all(b"e").await?;
self.w.flush().await?;
Ok(())
}
}
impl Sql {
/// Serializes the database into a bytestream.
pub async fn serialize(&self, w: impl AsyncWrite + Unpin) -> Result<()> {
let mut conn = self.get_connection().await?;
// Start a read transaction to take a database snapshot.
let transaction = conn.transaction()?;
let mut encoder = Encoder::new(transaction, w);
encoder.serialize().await?;
Ok(())
}
}
+1 -27
View File
@@ -429,12 +429,6 @@ pub enum StockMessage {
fallback = "⚠️ 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."
))]
CantDecryptOutgoingMsgs = 175,
#[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
MsgYouReacted = 176,
#[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
MsgReactedBy = 177,
}
impl StockMessage {
@@ -736,27 +730,6 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
}
}
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
pub(crate) async fn msg_reacted(
context: &Context,
by_contact: ContactId,
reaction: &str,
summary: &str,
) -> 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)
}
}
/// Stock string: `GIF`.
pub(crate) async fn gif(context: &Context) -> String {
translated(context, StockMessage::Gif).await
@@ -1448,6 +1421,7 @@ mod tests {
use super::*;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::test_utils::TestContext;
+125 -188
View File
@@ -10,9 +10,7 @@ use crate::context::Context;
use crate::message::{Message, MessageState, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::stock_str;
use crate::stock_str::msg_reacted;
use crate::tools::truncate;
use anyhow::Result;
/// Prefix displayed before message and separated by ":" in the chatlist.
#[derive(Debug)]
@@ -64,24 +62,7 @@ impl Summary {
msg: &Message,
chat: &Chat,
contact: Option<&Contact>,
) -> Result<Summary> {
if let Some((reaction_msg, reaction_contact_id, reaction)) = chat
.get_last_reaction_if_newer_than(context, msg.timestamp_sort)
.await?
{
// there is a reaction newer than the latest message, show that.
// sorting and therefore date is still the one of the last message,
// the reaction is is more sth. that overlays temporarily.
let summary = reaction_msg.get_summary_text_without_prefix(context).await;
return Ok(Summary {
prefix: None,
text: msg_reacted(context, reaction_contact_id, &reaction, &summary).await,
timestamp: msg.get_timestamp(), // message timestamp (not reaction) to make timestamps more consistent with chats ordering
state: msg.state, // message state (not reaction) - indicating if it was me sending the last message
thumbnail_path: None,
});
}
) -> Self {
let prefix = if msg.state == MessageState::OutDraft {
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
} else if msg.from_id == ContactId::SELF {
@@ -121,13 +102,13 @@ impl Summary {
None
};
Ok(Summary {
Self {
prefix,
text,
timestamp: msg.get_timestamp(),
state: msg.state,
thumbnail_path,
})
}
}
/// Returns the [`Summary::text`] attribute truncated to an approximate length.
@@ -139,125 +120,70 @@ impl Summary {
impl Message {
/// Returns a summary text.
async fn get_summary_text(&self, context: &Context) -> String {
let summary = self.get_summary_text_without_prefix(context).await;
if self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context).await, summary)
} else {
summary
}
}
/// Returns a summary text without "Forwarded:" prefix.
async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
let (emoji, type_name, type_file, append_text);
match self.viewtype {
Viewtype::Image => {
emoji = Some("📷");
type_name = Some(stock_str::image(context).await);
type_file = None;
append_text = true;
}
Viewtype::Gif => {
emoji = None;
type_name = Some(stock_str::gif(context).await);
type_file = None;
append_text = true;
}
Viewtype::Sticker => {
emoji = None;
type_name = Some(stock_str::sticker(context).await);
type_file = None;
append_text = true;
}
Viewtype::Video => {
emoji = Some("🎥");
type_name = Some(stock_str::video(context).await);
type_file = None;
append_text = true;
}
Viewtype::Voice => {
emoji = Some("🎤");
type_name = Some(stock_str::voice_message(context).await);
type_file = None;
append_text = true;
}
Viewtype::Audio => {
emoji = Some("🎵");
type_name = Some(stock_str::audio(context).await);
type_file = self.get_filename();
append_text = true
}
Viewtype::File => {
let mut append_text = true;
let prefix = match self.viewtype {
Viewtype::Image => stock_str::image(context).await,
Viewtype::Gif => stock_str::gif(context).await,
Viewtype::Sticker => stock_str::sticker(context).await,
Viewtype::Video => stock_str::video(context).await,
Viewtype::Voice => stock_str::voice_message(context).await,
Viewtype::Audio | Viewtype::File => {
if self.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
emoji = None;
type_name = Some(stock_str::ac_setup_msg_subject(context).await);
type_file = None;
append_text = false;
stock_str::ac_setup_msg_subject(context).await
} else {
emoji = Some("📎");
type_name = Some(stock_str::file(context).await);
type_file = self.get_filename();
append_text = true
let file_name = self
.get_filename()
.unwrap_or_else(|| String::from("ErrFileName"));
let label = if self.viewtype == Viewtype::Audio {
stock_str::audio(context).await
} else {
stock_str::file(context).await
};
format!("{label} – {file_name}")
}
}
Viewtype::VideochatInvitation => {
emoji = None;
type_name = Some(stock_str::videochat_invitation(context).await);
type_file = None;
append_text = false;
stock_str::videochat_invitation(context).await
}
Viewtype::Webxdc => {
emoji = None;
type_name = None;
type_file = Some(
self.get_webxdc_info(context)
.await
.map(|info| info.name)
.unwrap_or_else(|_| "ErrWebxdcName".to_string()),
);
append_text = true;
self.get_webxdc_info(context)
.await
.map(|info| info.name)
.unwrap_or_else(|_| "ErrWebxdcName".to_string())
}
Viewtype::Text | Viewtype::Unknown => {
emoji = None;
if self.param.get_cmd() == SystemMessage::LocationOnly {
type_name = Some(stock_str::location(context).await);
type_file = None;
append_text = false;
if self.param.get_cmd() != SystemMessage::LocationOnly {
"".to_string()
} else {
type_name = None;
type_file = None;
append_text = true;
append_text = false;
stock_str::location(context).await
}
}
};
let text = self.text.clone();
if !append_text {
return prefix;
}
let summary = if let Some(type_file) = type_file {
if append_text && !text.is_empty() {
format!("{type_file} – {text}")
} else {
type_file
}
} else if append_text && !text.is_empty() {
if emoji.is_some() {
text
} else if let Some(type_name) = type_name {
format!("{type_name} – {text}")
} else {
text
}
} else if let Some(type_name) = type_name {
type_name
let summary_content = if self.text.is_empty() {
prefix
} else if prefix.is_empty() {
self.text.to_string()
} else {
"".to_string()
format!("{prefix} – {}", self.text)
};
let summary = if let Some(emoji) = emoji {
format!("{emoji} {summary}")
let summary = if self.is_forwarded() {
format!(
"{}: {}",
stock_str::forwarded(context).await,
summary_content
)
} else {
summary
summary_content
};
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
@@ -270,11 +196,6 @@ mod tests {
use crate::param::Param;
use crate::test_utils as test;
async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
assert_eq!(msg.get_summary_text(ctx).await, expected);
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, expected);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_text() {
let d = test::TestContext::new().await;
@@ -284,81 +205,99 @@ mod tests {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(some_text.to_string());
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
assert_eq!(
msg.get_summary_text(ctx).await,
"bla bla" // for simple text, the type is not added to the summary
);
let mut msg = Message::new(Viewtype::Image);
msg.set_file("foo.jpg", None);
assert_summary_texts(&msg, ctx, "📷 Image").await; // file names are not added for images
let mut msg = Message::new(Viewtype::Image);
msg.set_text(some_text.to_string());
msg.set_file("foo.jpg", None);
assert_summary_texts(&msg, ctx, "📷 bla bla").await; // type is visible by emoji if text is set
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Image" // file names are not added for images
);
let mut msg = Message::new(Viewtype::Video);
msg.set_file("foo.mp4", None);
assert_summary_texts(&msg, ctx, "🎥 Video").await; // file names are not added for videos
let mut msg = Message::new(Viewtype::Video);
msg.set_text(some_text.to_string());
msg.set_file("foo.mp4", None);
assert_summary_texts(&msg, ctx, "🎥 bla bla").await; // type is visible by emoji if text is set
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Video" // file names are not added for videos
);
let mut msg = Message::new(Viewtype::Gif);
msg.set_file("foo.gif", None);
assert_summary_texts(&msg, ctx, "GIF").await; // file names are not added for GIFs
let mut msg = Message::new(Viewtype::Gif);
msg.set_text(some_text.to_string());
msg.set_file("foo.gif", None);
assert_summary_texts(&msg, ctx, "GIF \u{2013} bla bla").await; // file names are not added for GIFs
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"GIF" // file names are not added for GIFs
);
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file("foo.png", None);
assert_summary_texts(&msg, ctx, "Sticker").await; // file names are not added for stickers
let mut msg = Message::new(Viewtype::Voice);
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎤 Voice message").await; // file names are not added for voice messages
let mut msg = Message::new(Viewtype::Voice);
msg.set_text(some_text.clone());
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎤 bla bla").await;
let mut msg = Message::new(Viewtype::Audio);
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio
let mut msg = Message::new(Viewtype::Audio);
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio, empty text is not added
let mut msg = Message::new(Viewtype::Audio);
msg.set_text(some_text.clone());
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; // file name and text added for audio
let mut msg = Message::new(Viewtype::File);
msg.set_file("foo.bar", None);
assert_summary_texts(&msg, ctx, "📎 foo.bar").await; // file name is added for files
assert_eq!(
msg.get_summary_text(ctx).await,
"Sticker" // file names are not added for stickers
);
let mut msg = Message::new(Viewtype::Voice);
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Voice message" // file names are not added for voice messages, empty text is skipped
);
let mut msg = Message::new(Viewtype::Voice);
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Voice message" // file names are not added for voice messages
);
let mut msg = Message::new(Viewtype::Voice);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH"
);
let mut msg = Message::new(Viewtype::Audio);
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Audio \u{2013} foo.bar" // file name is added for audio
);
let mut msg = Message::new(Viewtype::Audio);
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added
);
let mut msg = Message::new(Viewtype::Audio);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio
);
let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; // file name is added for files
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
assert_eq!(
msg.get_summary_text(ctx).await,
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
);
// Forwarded
let mut msg = Message::new(Viewtype::Text);
msg.set_text(some_text.clone());
msg.param.set_int(Param::Forwarded, 1);
assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); // for simple text, the type is not added to the summary
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); // skipping prefix used for reactions summaries
assert_eq!(
msg.get_summary_text(ctx).await,
"Forwarded: bla bla" // for simple text, the type is not added to the summary
);
let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
@@ -366,17 +305,15 @@ mod tests {
msg.param.set_int(Param::Forwarded, 1);
assert_eq!(
msg.get_summary_text(ctx).await,
"Forwarded: 📎 foo.bar \u{2013} bla bla"
"Forwarded: File \u{2013} foo.bar \u{2013} bla bla"
);
assert_eq!(
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_text(some_text.clone());
msg.param.set(Param::File, "foo.bar");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
assert_summary_texts(&msg, ctx, "Autocrypt Setup Message").await; // file name is not added for autocrypt setup messages
assert_eq!(
msg.get_summary_text(ctx).await,
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
);
}
}
+3
View File
@@ -1,6 +1,7 @@
//! # Synchronize items between devices.
use anyhow::Result;
use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
@@ -320,9 +321,11 @@ mod tests {
use anyhow::bail;
use super::*;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::contact::{Contact, Origin};
use crate::test_utils::TestContext;
use crate::token::Namespace;
use crate::tools::SystemTime;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+2 -5
View File
@@ -177,7 +177,7 @@ async fn check_aeap_transition(
// Already add the new contact to one of the groups.
// We can then later check that the contact isn't in the group twice.
let already_new_contact = Contact::create(&bob, "Alice", ALICE_NEW_ADDR)
let already_new_contact = Contact::create(&bob, "Alice", "fiona@example.net")
.await
.unwrap();
if verified {
@@ -282,10 +282,7 @@ async fn check_that_transition_worked(
new_contact,
ContactId::SELF
);
assert!(
members.contains(&new_contact),
"Group {group} lacks {new_contact}"
);
assert!(members.contains(&new_contact));
assert!(members.contains(&ContactId::SELF));
let info_msg = get_last_info_msg(bob, *group).await.unwrap();
+1 -44
View File
@@ -1,7 +1,7 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::{self, add_contact_to_chat, Chat, ProtectionStatus};
use crate::chat::{self, Chat, ProtectionStatus};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING};
@@ -815,49 +815,6 @@ async fn test_create_protected_grp_multidev() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_member_added_reordering() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
enable_verified_oneonone_chats(&[alice, bob, fiona]).await;
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
// Bob and Fiona scan Alice's QR code.
tcm.execute_securejoin(bob, alice).await;
tcm.execute_securejoin(fiona, alice).await;
// Alice creates protected group with Bob.
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let alice_sent_group_promotion = alice.send_text(alice_chat_id, "I created a group").await;
let msg = bob.recv_msg(&alice_sent_group_promotion).await;
let bob_chat_id = msg.chat_id;
// Alice adds Fiona.
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
let alice_sent_member_added = alice.pop_sent_msg().await;
// Bob receives "Alice added Fiona" message.
bob.recv_msg(&alice_sent_member_added).await;
// Bob sends a message to the group.
let bob_sent_message = bob.send_text(bob_chat_id, "Hi").await;
// Fiona receives message from Bob before receiving
// "Member added" message.
let fiona_received_message = fiona.recv_msg(&bob_sent_message).await;
assert_eq!(
fiona_received_message.get_text(),
"[The message was sent with non-verified encryption. See 'Info' for more details]"
);
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
-19
View File
@@ -114,25 +114,6 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> Res
Ok(exists)
}
/// Looks up ChatId by auth token.
///
/// Returns None if auth token is not valid.
/// Returns zero/unset ChatId if the token corresponds to "setup contact" rather than group join.
pub async fn auth_chat_id(context: &Context, token: &str) -> Result<Option<ChatId>> {
let chat_id: Option<ChatId> = context
.sql
.query_row_optional(
"SELECT foreign_id FROM tokens WHERE namespc=? AND token=?",
(Namespace::Auth, token),
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
},
)
.await?;
Ok(chat_id)
}
pub async fn delete(context: &Context, namespace: Namespace, token: &str) -> Result<()> {
context
.sql
+25 -39
View File
@@ -181,7 +181,6 @@ pub fn get_release_timestamp() -> i64 {
*crate::release::DATE,
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
)
.and_utc()
.timestamp_millis()
/ 1_000
}
@@ -206,7 +205,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
),
)
.await;
if let Some(timestamp) = chrono::DateTime::<chrono::Utc>::from_timestamp(now, 0) {
if let Some(timestamp) = chrono::NaiveDateTime::from_timestamp_opt(now, 0) {
add_device_msg_with_importance(
context,
Some(
@@ -233,7 +232,7 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::update_reminder_msg_body(context).await;
if let Some(timestamp) = chrono::DateTime::<chrono::Utc>::from_timestamp(now, 0) {
if let Some(timestamp) = chrono::NaiveDateTime::from_timestamp_opt(now, 0) {
add_device_msg(
context,
Some(
@@ -278,20 +277,19 @@ pub(crate) fn create_id() -> String {
.collect()
}
/// Returns true if given string is a valid ID.
///
/// All IDs generated with `create_id()` should be considered valid.
pub(crate) fn validate_id(s: &str) -> bool {
let alphabet = base64::alphabet::URL_SAFE.as_str();
s.chars().all(|c| alphabet.contains(c)) && s.len() > 10 && s.len() <= 32
}
/// Function generates a Message-ID that can be used for a new outgoing message.
/// - this function is called for all outgoing messages.
/// - the message ID should be globally unique
/// - do not add a counter or any private data as this leaks information unnecessarily
pub(crate) fn create_outgoing_rfc724_mid() -> String {
format!("Mr.{}.{}@localhost", create_id(), create_id())
pub(crate) fn create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
let hostname = from_addr
.find('@')
.and_then(|k| from_addr.get(k..))
.unwrap_or("@nohost");
match grpid {
Some(grpid) => format!("Gr.{}.{}{}", grpid, create_id(), hostname),
None => format!("Mr.{}.{}{}", create_id(), create_id(), hostname),
}
}
/// Extract the group id (grpid) from a message id (mid)
@@ -971,27 +969,6 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
assert_eq!(buf.len(), 11);
}
#[test]
fn test_validate_id() {
for _ in 0..10 {
assert!(validate_id(&create_id()));
}
assert_eq!(validate_id("aaaaaaaaaaaa"), true);
assert_eq!(validate_id("aa-aa_aaaXaa"), true);
// ID cannot contain whitespace.
assert_eq!(validate_id("aaaaa aaaaaa"), false);
assert_eq!(validate_id("aaaaa\naaaaaa"), false);
// ID cannot contain "/", "+".
assert_eq!(validate_id("aaaaa/aaaaaa"), false);
assert_eq!(validate_id("aaaaaaaa+aaa"), false);
// Too long ID.
assert_eq!(validate_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), false);
}
#[test]
fn test_create_id_invalid_chars() {
for _ in 1..1000 {
@@ -1036,10 +1013,21 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
#[test]
fn test_create_outgoing_rfc724_mid() {
let mid = create_outgoing_rfc724_mid();
// create a normal message-id
let mid = create_outgoing_rfc724_mid(None, "foo@bar.de");
assert!(mid.starts_with("Mr."));
assert!(mid.ends_with("@localhost"));
assert!(mid.ends_with("bar.de"));
assert!(extract_grpid_from_rfc724_mid(mid.as_str()).is_none());
// create a message-id containing a group-id
let grpid = create_id();
let mid = create_outgoing_rfc724_mid(Some(&grpid), "foo@bar.de");
assert!(mid.starts_with("Gr."));
assert!(mid.ends_with("bar.de"));
assert_eq!(
extract_grpid_from_rfc724_mid(mid.as_str()),
Some(grpid.as_str())
);
}
#[test]
@@ -1076,7 +1064,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
use chrono::NaiveDate;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use proptest::prelude::*;
use crate::chatlist::Chatlist;
@@ -1215,7 +1203,6 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
NaiveDate::from_ymd_opt(2020, 9, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
)
.and_utc()
.timestamp_millis()
/ 1_000;
@@ -1331,7 +1318,6 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
)
.and_utc()
.timestamp_millis()
/ 1_000;
assert!(get_release_timestamp() <= time());
+3 -3
View File
@@ -164,7 +164,7 @@ mod tests {
To: alice@example.org\n\
Message-ID: <msg1@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde123456\n\
Chat-Group-ID: abcde\n\
Chat-Group-Name: initial name\n\
Date: Sun, 22 Mar 2021 01:00:00 +0000\n\
\n\
@@ -182,7 +182,7 @@ mod tests {
To: alice@example.org\n\
Message-ID: <msg3@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde123456\n\
Chat-Group-ID: abcde\n\
Chat-Group-Name: another name update\n\
Chat-Group-Name-Changed: a name update\n\
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
@@ -197,7 +197,7 @@ mod tests {
To: alice@example.org\n\
Message-ID: <msg2@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde123456\n\
Chat-Group-ID: abcde\n\
Chat-Group-Name: a name update\n\
Chat-Group-Name-Changed: initial name\n\
Date: Sun, 22 Mar 2021 02:00:00 +0000\n\
+1
View File
@@ -20,6 +20,7 @@ use std::path::Path;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
use deltachat_derive::FromSql;
use lettre_email::mime;
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
use serde_json::Value;
-28
View File
@@ -1,28 +0,0 @@
Return-Path: <wwwrun@xpressus.emsmtp.com>
Delivered-To: alice@example.org
To: alice@example.org
Subject: Vorteile sind zum Teilen da!
X-Mailer: class SMTPMail
From: "=?ISO-8859-1?Q?=D6AMTC?=" <clubinfo@donotreply.oeamtc.at>
MIME-Version: 1.0
List-Id: 134722648 <=?ISO-8859-1?Q?=D6AMTC?=>
List-Unsubscribe: <mailto:list-unsubscribe+xxx@emarsys.net>, <https://list-unsubscribe.eservice.emarsys.net/api/unsubscribe/xxx>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Content-Type: multipart/alternative;
boundary="--=_---NextPart--=_-wFcVesztLV"
Message-ID: <foobarbaz@pmta41019.emsmtp.com>
Date: Wed, 20 Mar 2024 10:00:01 +0100
----=_---NextPart--=_-wFcVesztLV
Content-Type: text/plain; charset=iso-8859-1
Content-transfer-encoding: quoted-printable
Plain text spam
----=_---NextPart--=_-wFcVesztLV
Content-Type: text/html; charset=iso-8859-1
Content-transfer-encoding: quoted-printable
HTML SPAM
----=_---NextPart--=_-wFcVesztLV--

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