mirror of
https://github.com/chatmail/core.git
synced 2026-08-18 06:36:29 +03:00
Compare commits
1 Commits
d2weber/qt
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8056605e37 |
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -43,7 +43,6 @@ jobs:
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
@@ -62,7 +61,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe
|
||||
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb
|
||||
with:
|
||||
arguments: --workspace --all-features --locked
|
||||
command: check
|
||||
@@ -97,7 +96,6 @@ jobs:
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
- name: Rustdoc
|
||||
run: cargo doc --document-private-items --no-deps
|
||||
|
||||
@@ -143,10 +141,9 @@ jobs:
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154
|
||||
uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -180,10 +177,9 @@ jobs:
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
|
||||
- name: Build C library
|
||||
run: cargo build -p deltachat_ffi --locked
|
||||
run: cargo build -p deltachat_ffi
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -209,10 +205,9 @@ jobs:
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
|
||||
- name: Build deltachat-rpc-server
|
||||
run: cargo build -p deltachat-rpc-server --locked
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
14
.github/workflows/deltachat-rpc-server.yml
vendored
14
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v7
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install dependencies without running scripts
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install --ignore-scripts
|
||||
|
||||
- name: Package
|
||||
@@ -43,5 +43,5 @@ jobs:
|
||||
npm pack .
|
||||
|
||||
- name: Publish
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||
|
||||
13
.github/workflows/jsonrpc.yml
vendored
13
.github/workflows/jsonrpc.yml
vendored
@@ -25,21 +25,18 @@ jobs:
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
- name: npm install
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install
|
||||
- name: Build TypeScript, run Rust tests, generate bindings
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run build
|
||||
- name: Run integration tests
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run test
|
||||
env:
|
||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||
- name: Run linter
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run prettier:check
|
||||
|
||||
7
.github/workflows/nix.yml
vendored
7
.github/workflows/nix.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -63,6 +63,7 @@ jobs:
|
||||
- deltachat-rpc-server-armv7l-linux-wheel
|
||||
- deltachat-rpc-server-i686-linux
|
||||
- deltachat-rpc-server-i686-linux-wheel
|
||||
- deltachat-rpc-server-source
|
||||
- deltachat-rpc-server-win32
|
||||
- deltachat-rpc-server-win32-wheel
|
||||
- deltachat-rpc-server-win64
|
||||
@@ -83,7 +84,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -104,5 +105,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
8
.github/workflows/upload-docs.yml
vendored
8
.github/workflows/upload-docs.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc-bindings/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
|
||||
|
||||
build-cffi:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -23,4 +23,4 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
|
||||
271
CHANGELOG.md
271
CHANGELOG.md
@@ -1,272 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.53.0] - 2026-06-15
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Make quality of images sent in chats more consistent between images with different aspect ratio.
|
||||
- `MsgId::get_html`: Make only one db query.
|
||||
- Do not log the recipient list for sent messages.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not trash pre-messages without text but with a webxdc update.
|
||||
- Don't send or process webxdc status updates in pre-messages.
|
||||
- Ignore SecureJoin messages from blocked contacts ([#8295](https://github.com/chatmail/core/pull/8295)).
|
||||
- Do not abort IMAP connection if setting the push token fails.
|
||||
|
||||
### Documentation
|
||||
|
||||
- STYLE.md: Require to list columns explicitly in `INSERT` statements.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: switch to the "master" branch for naersk.
|
||||
- flake.nix: Use hostPlatform.rust.rustcTarget instead of hardcoding it.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Bump version to 2.52.0-dev.
|
||||
- deps: bump taiki-e/install-action from 2.79.10 to 2.81.1.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.19 to 2.0.20.
|
||||
- Bump version to 2.53.0-dev.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move the definition of the `target_wh`-variable.
|
||||
- Remove timesmearing.
|
||||
|
||||
### Tests
|
||||
|
||||
- Print multiline chat descriptions with debug formatter.
|
||||
- `exec_securejoin_qr_multi_device()`: Make inviter devices receive each other messages.
|
||||
- Fixup the tests after removing timesmearing.
|
||||
- Remove timeout from `pop_sent_msg_ex()`.
|
||||
|
||||
## [2.52.0] - 2026-06-09
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update the channel title after joining if the QR code included a wrong title ([#8260](https://github.com/chatmail/core/pull/8260)).
|
||||
- Don't send removal message to contact that hasn't been a chat member ([#8298](https://github.com/chatmail/core/pull/8298)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add cryptography-related statistics (`number_of_transports`, `key_version`, `key_algorithm`, `pubkey_size`, `number_of_keys`) ([#8293](https://github.com/chatmail/core/pull/8293), [#8297](https://github.com/chatmail/core/pull/8297)).
|
||||
- Add IMAP folder to `Context::get_info()` ([#8285](https://github.com/chatmail/core/pull/8285)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update preloaded DNS cache.
|
||||
- Use default aws-lc-rs cryptography provider for rustls.
|
||||
- Add exception for unmaintained proc-macro-error2 to deny.toml.
|
||||
- cargo: bump `pin-project` from 1.1.11 to 1.1.13.
|
||||
- cargo: bump `tokio` from 1.52.1 to 1.52.3.
|
||||
- cargo: bump `log` from 0.4.29 to 0.4.30.
|
||||
- cargo: bump `serde_json` from 1.0.149 to 1.0.150.
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.18 to 2.0.19.
|
||||
- deps: bump taiki-e/install-action from 2.79.2 to 2.79.10.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: fix windows cross-compilation by adding pthreads includes.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove support for building "source" packages for deltachat-rpc-server.
|
||||
|
||||
## [2.51.0] - 2026-05-29
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Follow certificate check parameter in autoconfig.
|
||||
- Immediately remove all encrypted messages from the server in single-device mode.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix syntax error in `only_fetch_mvbox` migration 150 resulting in failure to upgrade for `only_fetch_mvbox` users.
|
||||
- Do not try to resolve proxy IPv6 addresses in square brackets.
|
||||
- Do not fail to receive post-message with status updates for deleted webxdc.
|
||||
- Don't make message `OutDelivered` after successful resending to new broadcast member.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: fix downloads from crates.io in nix builds.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix reference in `delete_expired_imap_messages` comment.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `pre_encrypt_mime_hook`.
|
||||
- Make `should_delete_all_downloaded_messages` non-async.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test IPv6 addresses in HTTP(S) proxies.
|
||||
- Test `bcc_self` in `test_delete_expired_imap_messages`.
|
||||
- Test encrypted messages in `test_delete_expired_imap_messages`.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Bump version to 2.51.0-dev.
|
||||
- deps: bump zizmorcore/zizmor-action from 0.5.3 to 0.5.6.
|
||||
- deps: bump taiki-e/install-action from 2.78.1 to 2.79.2.
|
||||
|
||||
## [2.50.0] - 2026-05-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC APIs for location streaming.
|
||||
- [**breaking**] Remove unused config `smtp_certificate_checks`.
|
||||
- Deprecate old server config keys that were replaced by `add_or_update_transport()`.
|
||||
- [**breaking**] remove `dc_delete_all_locations`.
|
||||
- [**breaking**] Remove unused `info_only` option when loading a chatlist ([#8171](https://github.com/chatmail/core/pull/8171)).
|
||||
- [**breaking**] location: avoid repeating module name in function names
|
||||
- [**breaking**] deltachat-rpc-client: remove deprecated `get_fresh_messages_in_arrival_order()`.
|
||||
- Remove unused `set_draft_vcard()` JSON-RPC API.
|
||||
- Remove mostly-unused `sign_unencrypted` config ([#8190](https://github.com/chatmail/core/pull/8190)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- [**breaking**] Remove `mvbox_move` and `only_fetch_mvbox` configs. Transports have `folder` configuration to watch the folder other than `INBOX` as a replacement for `only_fetch_mvbox`. Non-chatmail transports no longer watch mvbox, each transport watches exactly one folder now.
|
||||
- Add `force_encryption` config to ignore incoming unencrypted messages and enforce encryption for outgoing messages.
|
||||
- Remove "Delete Messages from Server" (`delete_server_after`) config ([#8240](https://github.com/chatmail/core/pull/8240)).
|
||||
- Remove `show_emails` config.
|
||||
- Remove non-sticker heuristics and `force_sticker()`. UIs should make sure not to send images from gallery such as screenshots as stickers.
|
||||
- Enable PQC (Post-Quantum Cryptography) support for OpenPGP. We do not generate PQC keys yet, this step is needed for forward compatibility.
|
||||
- Resend the last 10 messages to new broadcast member ([#8151](https://github.com/chatmail/core/pull/8151)).
|
||||
- Allow TLS connections with invalid certificate if the key is unchanged.
|
||||
- Add `is_app_sender` and `is_broadcast` contexts for webxdc.
|
||||
- Increase the resolution-limit `WORSE_AVATAR_SIZE` from 128 to 256.
|
||||
- Change multiplier to 7/8 when scaling down avatars.
|
||||
- Add error cause to connectivity view for IMAP errors.
|
||||
- Remove the largely-unused ability to send multiple reactions to one message ([#8131](https://github.com/chatmail/core/pull/8131)).
|
||||
- Don't show non-delivery-notfications in broadcast channels ([#8159](https://github.com/chatmail/core/pull/8159)).
|
||||
- Adapt quota warning to automatic cleanup.
|
||||
- Remove `Content-Description` and `Content-Disposition` from `multipart/encrypted` parts.
|
||||
- Log all connection attempt errors instead of the first one.
|
||||
- Remove workaround for old filtermail (part of chatmail relay) which expected exact number of newlines in OpenPGP messages.
|
||||
- Remove key fingerprint from `Context.get_info()`.
|
||||
- Mask local part of email addresses in `used_transport_settings`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Trash no-op messages about self being added to groups.
|
||||
- `decide_chat_assignment`: Log correct `post_msg_exists` value.
|
||||
- Don't send `Chat-Group-Name*` headers for InBroadcast-s.
|
||||
- Restart io on transport deletion.
|
||||
- Never remove primary transport when applying `SyncTransports` message.
|
||||
- Set Param::GuaranteeE2ee before preparing message blob ([#8090](https://github.com/chatmail/core/pull/8090)).
|
||||
- `fetch_single_msg()`: Lock `fetch_msgs_mutex` before fetching.
|
||||
- Set dir to "auto" in body tag when converting plain-text to HTML ([#8227](https://github.com/chatmail/core/pull/8227)).
|
||||
- Scale up contacts messaged in groups to `IncomingTo`.
|
||||
- Do not sort prefetched messages by INTERNALDATE.
|
||||
- Don't resort re-sent message to the bottom ([#8145](https://github.com/chatmail/core/pull/8145)).
|
||||
- Ensure that message being sent is added to the bottom ([#8027](https://github.com/chatmail/core/pull/8027)).
|
||||
- Don't receive message if a deletion request was received before ([#8143](https://github.com/chatmail/core/pull/8143)).
|
||||
- Emit `MsgsChanged`, not `IncomingMsg`, for messages only having special parts ([#8157](https://github.com/chatmail/core/pull/8157)).
|
||||
- Generate new pre-message `Message-ID` when forwarding.
|
||||
- use correct dir converting plaintext to HTML ([#8248](https://github.com/chatmail/core/pull/8248)).
|
||||
- hide connectivity HTML quota if not supported.
|
||||
- Delete pre-messages on the server for single-device chatmail transports ([#8240](https://github.com/chatmail/core/pull/8240)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Upgrade rustls-webpki to 0.103.12.
|
||||
- Remove coredeps `Dockerfile`.
|
||||
- Increase MSRV to 1.89.
|
||||
|
||||
### CI
|
||||
|
||||
- Remove Concourse CI pipelines.
|
||||
- Update Rust to 1.95.0.
|
||||
- Do not store Rust cache from PRs.
|
||||
- Set cache-bin to "false" for `swatinem/rust-cache` action.
|
||||
- Use `--locked` flag with `cargo build`.
|
||||
- Upgrade `cargo-deny-action` to v2.0.17.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update `echobot_no_hooks.py` example.
|
||||
- Discourage `into()`, `try_into()` and `parse()` ([#8180](https://github.com/chatmail/core/pull/8180)).
|
||||
- Remove outdated comment about "quota warning" device message.
|
||||
- Update README.md: Use ci-chatmail instead of nine ([#8238](https://github.com/chatmail/core/pull/8238)).
|
||||
- spec: remove AEAP section.
|
||||
|
||||
### Performance
|
||||
|
||||
- Enable `clippy::large_futures` lint.
|
||||
- Stop sending locations concurrently.
|
||||
- Set location for all accounts in parallel.
|
||||
- `is_self_addr()`: Employ the config cache to optimize for `ConfiguredAddr` passed.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Get rid of `MessageState::{OutPreparing,OutMdnRcvd}` in the db.
|
||||
- Make HTML parser non-async.
|
||||
- Replace `HashSet` with `BTreeSet`.
|
||||
- Rename `EnteredLoginParam::load()` and save() to `load_legacy()` and `save_legacy()`.
|
||||
- Remove unnecessary async block in `dc_set_location`.
|
||||
- Remove unused Authentication-Results parsing ([#8172](https://github.com/chatmail/core/pull/8172)).
|
||||
- Remove mostly-unused function `get_secondary_self_addrs()` ([#8173](https://github.com/chatmail/core/pull/8173)).
|
||||
- Use `self_fingerprint()` where it makes sense ([#8174](https://github.com/chatmail/core/pull/8174)).
|
||||
- Split `is_sending_locations_to_chat()` into two functions.
|
||||
- Use regular functions rather than FromStr impls ([#8178](https://github.com/chatmail/core/pull/8178)).
|
||||
- Make `Fingerprint` not implement `Display` ([#8177](https://github.com/chatmail/core/pull/8177)).
|
||||
- Don't temporarily extend `signatures` for signed-only messages.
|
||||
- Use some more let..else.
|
||||
- Remove outdated comment.
|
||||
- Un-nest `handle_edit_delete`.
|
||||
- Drop support for replacing partial download stubs.
|
||||
|
||||
### Tests
|
||||
|
||||
- Use `displayname` instead of `show_emails` for config cache test.
|
||||
- Remove unused test data related to Authentication-Result parsing ([#8175](https://github.com/chatmail/core/pull/8175)).
|
||||
- `EventTracker::get_matching_opt`: Return the first matching event, not last.
|
||||
- Set email addresses explicitly for the test accounts.
|
||||
- Use encrypted messages in more tests.
|
||||
- Add `TestContext.allow_unencrypted()`.
|
||||
- Online test for legacy Secure-Join key request.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: bump rand from 0.9.2 to 0.9.3.
|
||||
- deps: bump taiki-e/install-action from 2.64.0 to 2.74.0.
|
||||
- add exception for RUSTSEC-2026-0097.
|
||||
- deps: bump swatinem/rust-cache from 2.8.2 to 2.9.1.
|
||||
- cargo: upgrade rand 0.8.5 to rand 0.8.6.
|
||||
- deps: bump zizmorcore/zizmor-action from 0.5.2 to 0.5.3.
|
||||
- deps: bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0.
|
||||
- update provider database.
|
||||
- cargo: update rustls-webpki to 0.103.13.
|
||||
- cargo: bump openssl from 0.10.72 to 0.10.78.
|
||||
- Apply rustmft after the previous commit.
|
||||
- json-rpc: deprecate `send_sticker` ([#8189](https://github.com/chatmail/core/pull/8189)).
|
||||
- deps: bump cachix/install-nix-action from 31.9.1 to 31.10.5.
|
||||
- deps: bump taiki-e/install-action from 2.75.10 to 2.75.19.
|
||||
- update astral-tokio-tar from 0.6.0 to 0.6.1.
|
||||
- add exceptions for hickory-proto 0.25.2 in deny.toml.
|
||||
- cargo: bump blake3 from 1.8.3 to 1.8.5.
|
||||
- deny.toml: add cpufeatures duplicate dependency exception.
|
||||
- cargo: bump hyper from 1.8.1 to 1.9.0.
|
||||
- cargo: bump tokio from 1.50.0 to 1.52.1.
|
||||
- cargo: bump libc from 0.2.184 to 0.2.186.
|
||||
- cargo: bump colorutils-rs from 0.7.6 to 0.8.0.
|
||||
- cargo: bump data-encoding from 2.10.0 to 2.11.0.
|
||||
- cargo: bump openssl from 0.10.78 to 0.10.79.
|
||||
- deps: bump taiki-e/install-action from 2.75.19 to 2.77.1.
|
||||
- deps: bump cachix/install-nix-action from 31.10.5 to 31.10.6.
|
||||
- allow passing arguments to scripts/clippy.sh.
|
||||
- clippy::useless-borrows-in-formatting fixes.
|
||||
- update zerocopy from 0.7.32 to 0.7.35.
|
||||
- upgrade astral-tokio-tar to 0.6.2 ([#8255](https://github.com/chatmail/core/pull/8255)).
|
||||
- deps: bump EmbarkStudios/cargo-deny-action from 2.0.17 to 2.0.18.
|
||||
- cargo: bump openssl from 0.10.79 to 0.10.80.
|
||||
- deps: bump taiki-e/install-action from 2.77.1 to 2.78.1.
|
||||
|
||||
## [2.49.0] - 2026-04-13
|
||||
|
||||
### Features / Changes
|
||||
@@ -8370,7 +8103,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.47.0]: https://github.com/chatmail/core/compare/v2.46.0..v2.47.0
|
||||
[2.48.0]: https://github.com/chatmail/core/compare/v2.47.0..v2.48.0
|
||||
[2.49.0]: https://github.com/chatmail/core/compare/v2.48.0..v2.49.0
|
||||
[2.50.0]: https://github.com/chatmail/core/compare/v2.49.0..v2.50.0
|
||||
[2.51.0]: https://github.com/chatmail/core/compare/v2.50.0..v2.51.0
|
||||
[2.52.0]: https://github.com/chatmail/core/compare/v2.51.0..v2.52.0
|
||||
[2.53.0]: https://github.com/chatmail/core/compare/v2.52.0..v2.53.0
|
||||
|
||||
@@ -2,51 +2,45 @@ cmake_minimum_required(VERSION 3.16)
|
||||
project(deltachat LANGUAGES C)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
option(WITH_JSONRPC_BINDINGS "Generate jsonrpc bindings" OFF)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
if(APPLE)
|
||||
set(DYNAMIC_EXT "dylib")
|
||||
set(DYNAMIC_EXT "dylib")
|
||||
elseif(UNIX)
|
||||
set(DYNAMIC_EXT "so")
|
||||
set(DYNAMIC_EXT "so")
|
||||
else()
|
||||
set(DYNAMIC_EXT "dll")
|
||||
set(DYNAMIC_EXT "dll")
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
||||
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/$ENV{CARGO_BUILD_TARGET}/release")
|
||||
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||
else()
|
||||
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/release")
|
||||
set(ARCH_DIR "./")
|
||||
endif()
|
||||
|
||||
if(WITH_JSONRPC_BINDINGS)
|
||||
set(JSONRPC_ARGS --package deltachat-jsonrpc-bindings)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
lib_deltachat
|
||||
ALL
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E env
|
||||
CARGO_TARGET_DIR="${CMAKE_BINARY_DIR}/target"
|
||||
PREFIX="${CMAKE_INSTALL_PREFIX}"
|
||||
LIBDIR="${CMAKE_INSTALL_FULL_LIBDIR}"
|
||||
INCLUDEDIR="${CMAKE_INSTALL_FULL_INCLUDEDIR}"
|
||||
${CARGO} build --release --package deltachat_ffi ${JSONRPC_ARGS}
|
||||
WORKING_DIRECTORY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
|
||||
install(FILES "${CARGO_OUT_DIR}/libdeltachat.a" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
|
||||
install(FILES "${CARGO_OUT_DIR}/libdeltachat.${DYNAMIC_EXT}" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
|
||||
install(FILES "${CARGO_OUT_DIR}/pkgconfig/deltachat.pc" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
|
||||
add_custom_target(
|
||||
lib_deltachat
|
||||
ALL
|
||||
DEPENDS
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
if(WITH_JSONRPC_BINDINGS)
|
||||
set(JSONRPC_HPP "${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc")
|
||||
install(FILES "${JSONRPC_HPP}/generated/types.hpp" "${JSONRPC_HPP}/generated/client.hpp"
|
||||
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc/generated")
|
||||
install(FILES "${JSONRPC_HPP}/cffi_client.hpp"
|
||||
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc")
|
||||
endif()
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
||||
257
Cargo.lock
generated
257
Cargo.lock
generated
@@ -194,9 +194,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.6.2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277"
|
||||
checksum = "4ce73b17c62717c4b6a9af10b43e87c578b0cac27e00666d48304d3b7d2c0693"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"futures-core",
|
||||
@@ -391,28 +391,6 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backon"
|
||||
version = "1.5.0"
|
||||
@@ -785,13 +763,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.63"
|
||||
version = "1.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
||||
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@@ -945,15 +920,6 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.2.3"
|
||||
@@ -968,9 +934,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorutils-rs"
|
||||
version = "0.8.0"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69abc9a8ed011e2b7946769f460b9e76e8b659ece9ef4001b9d8bba3489f796d"
|
||||
checksum = "6e2fc25857fa523662de5cae84225b0e7bfb24a2a3f9ed8802fecf03df7252b1"
|
||||
dependencies = [
|
||||
"erydanos",
|
||||
"half",
|
||||
@@ -1335,9 +1301,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "dbl"
|
||||
@@ -1350,7 +1316,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1434,7 +1400,7 @@ dependencies = [
|
||||
"url",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"webpki-roots",
|
||||
"webpki-roots 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1459,7 +1425,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1478,16 +1444,9 @@ dependencies = [
|
||||
"yerpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.54.0-dev"
|
||||
dependencies = [
|
||||
"deltachat-jsonrpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1503,7 +1462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1532,7 +1491,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1717,7 +1676,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1767,12 +1726,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.18"
|
||||
@@ -2098,12 +2051,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
@@ -2161,12 +2108,6 @@ dependencies = [
|
||||
name = "format-flowed"
|
||||
version = "1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@@ -2667,25 +2608,6 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
@@ -2723,7 +2645,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3087,7 +3009,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"wasm-bindgen-futures",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
"x509-parser",
|
||||
"z32",
|
||||
]
|
||||
@@ -3273,7 +3195,7 @@ dependencies = [
|
||||
"tokio-websockets",
|
||||
"tracing",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
"ws_stream_wasm",
|
||||
"z32",
|
||||
]
|
||||
@@ -3293,16 +3215,6 @@ version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
@@ -3345,16 +3257,6 @@ dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kem"
|
||||
version = "0.3.0-pre.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -3443,9 +3345,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.31"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@@ -3568,35 +3470,6 @@ dependencies = [
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-dsa"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac4a46643af2001eafebcc37031fc459eb72d45057aac5d7a15b00046a2ad6db"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"hybrid-array 0.3.1",
|
||||
"num-traits",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha3",
|
||||
"signature",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-kem"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f"
|
||||
dependencies = [
|
||||
"hybrid-array 0.2.3",
|
||||
"kem",
|
||||
"rand_core 0.6.4",
|
||||
"sha3",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.10"
|
||||
@@ -3887,7 +3760,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4068,14 +3941,15 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.80"
|
||||
version = "0.10.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
|
||||
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
@@ -4108,9 +3982,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.116"
|
||||
version = "0.9.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
|
||||
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -4332,8 +4206,6 @@ dependencies = [
|
||||
"k256",
|
||||
"log",
|
||||
"md-5",
|
||||
"ml-dsa",
|
||||
"ml-kem",
|
||||
"nom 8.0.0",
|
||||
"num-bigint-dig",
|
||||
"num-traits",
|
||||
@@ -4352,7 +4224,6 @@ dependencies = [
|
||||
"sha2",
|
||||
"sha3",
|
||||
"signature",
|
||||
"slh-dsa",
|
||||
"smallvec",
|
||||
"snafu",
|
||||
"twofish",
|
||||
@@ -4372,18 +4243,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.13"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
|
||||
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.13"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
|
||||
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5136,7 +5007,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
@@ -5288,7 +5159,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5297,7 +5168,6 @@ version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
@@ -5343,7 +5213,6 @@ version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -5588,9 +5457,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -5766,9 +5635,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
@@ -5819,25 +5688,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slh-dsa"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
"hmac",
|
||||
"hybrid-array 0.3.1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha2",
|
||||
"sha3",
|
||||
"signature",
|
||||
"typenum",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -6154,7 +6004,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6302,9 +6152,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
version = "1.52.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -6915,9 +6765,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.8"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -7514,7 +7373,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "yerpc"
|
||||
version = "0.6.4"
|
||||
source = "git+https://github.com/d2weber/yerpc.git?branch=qt_bindings#ba7aead1ffa49dc89bb85322aba38a65a0694e7b"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dc24983fbe850227bfc1de89bf8cbfb3e2463afc322e0de2f155c4c23d06445"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 1.9.0",
|
||||
@@ -7532,8 +7392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yerpc_derive"
|
||||
version = "0.6.4"
|
||||
source = "git+https://github.com/d2weber/yerpc.git?branch=qt_bindings#ba7aead1ffa49dc89bb85322aba38a65a0694e7b"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d8560d021437420316370db865e44c000bf86380b47cf05e49be9d652042bf5"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"darling",
|
||||
@@ -7574,9 +7435,9 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
@@ -7584,9 +7445,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
@@ -53,7 +53,7 @@ blake3 = "1.8.2"
|
||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
colorutils-rs = { version = "0.8.0", default-features = false }
|
||||
colorutils-rs = { version = "0.7.5", default-features = false }
|
||||
data-encoding = "2.9.0"
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "1"
|
||||
@@ -78,7 +78,7 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
|
||||
pgp = { version = "0.19.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||
@@ -101,9 +101,9 @@ tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["aws-lc-rs", "tls12"] }
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6.2", default-features = false }
|
||||
astral-tokio-tar = { version = "0.6.1", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
@@ -111,7 +111,7 @@ tracing = "0.1.41"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
walkdir = "2.5.0"
|
||||
webpki-roots = "0.26.8"
|
||||
webpki-roots = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -130,7 +130,6 @@ members = [
|
||||
"deltachat-ffi",
|
||||
"deltachat_derive",
|
||||
"deltachat-jsonrpc",
|
||||
"deltachat-jsonrpc-bindings",
|
||||
"deltachat-rpc-server",
|
||||
"deltachat-ratelimit",
|
||||
"deltachat-repl",
|
||||
@@ -185,7 +184,6 @@ base64 = "0.22"
|
||||
chrono = { version = "0.4.44", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat-jsonrpc-bindings = { path = "deltachat-jsonrpc-bindings", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.32"
|
||||
futures-lite = "2.6.1"
|
||||
@@ -205,7 +203,7 @@ thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.18"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = { git="https://github.com/d2weber/yerpc.git", branch="qt_bindings" }
|
||||
yerpc = "0.6.4"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
7
STYLE.md
7
STYLE.md
@@ -59,13 +59,6 @@ If column is already declared without `NOT NULL`, use `IFNULL` function to provi
|
||||
Use `HAVING COUNT(*) > 0` clause
|
||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
||||
|
||||
List columns explicitly in `INSERT` statements:
|
||||
```
|
||||
INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0);
|
||||
```
|
||||
Otherwise if a new column with default value is added in a future DB version, an upgraded DB can't
|
||||
be used with the old code, e.g. after transferring a DB from a device running a newer version.
|
||||
|
||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
||||
an older version. Also don't change the column type, consider adding a new column with another name
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -413,6 +413,11 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
||||
* 1=delete messages directly after receiving from server, mvbox is skipped.
|
||||
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
|
||||
* "Saved messages" are deleted from the server as well as emails, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
||||
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
||||
* DC_MEDIA_QUALITY_WORSE (1)
|
||||
@@ -482,7 +487,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 0 = Everybody (except explicitly blocked contacts),
|
||||
* 1 = Contacts (default, does not include contact requests),
|
||||
* 2 = Nobody (calls never result in a notification).
|
||||
* - `force_encryption` = 1 (default) to force encryption, 0 to allow unencrypted messages.
|
||||
*
|
||||
* Also, there are configs that are only needed
|
||||
* if you want to use the deprecated dc_configure() API, such as:
|
||||
@@ -1457,16 +1461,16 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
* by the dc_set_config()-option `delete_device_after`.
|
||||
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
* This is typically used to show the estimated impact to the user
|
||||
* before actually enabling deletion of old messages.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param from_server Deprecated, pass 0 here
|
||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
||||
* @param seconds Count messages older than the given number of seconds.
|
||||
* @return Number of messages that are older than the given number of seconds.
|
||||
* Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
*/
|
||||
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
|
||||
|
||||
@@ -3999,6 +4003,8 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
|
||||
* Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
|
||||
*
|
||||
* Outgoing message states:
|
||||
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
|
||||
* the message enters this state before @ref DC_STATE_OUT_PENDING. Deprecated.
|
||||
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
|
||||
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
|
||||
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
|
||||
@@ -5583,6 +5589,13 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
#define DC_STATE_IN_SEEN 16
|
||||
|
||||
/**
|
||||
* Outgoing message being prepared. See dc_msg_get_state() for details.
|
||||
*
|
||||
* @deprecated 2024-12-07
|
||||
*/
|
||||
#define DC_STATE_OUT_PREPARING 18
|
||||
|
||||
/**
|
||||
* Outgoing message drafted. See dc_msg_get_state() for details.
|
||||
*/
|
||||
@@ -6341,15 +6354,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_MSG_DELETED 2016
|
||||
|
||||
/**
|
||||
* Like @ref DC_EVENT_MSG_READ, but also fires on subsequent MDNs,
|
||||
* if there are multiple receivers, i.e. in groups and channels.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
*/
|
||||
#define DC_EVENT_MSG_READ_COUNT_CHANGED 2018
|
||||
|
||||
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
@@ -7061,6 +7065,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
|
||||
#define DC_STR_ERROR 112
|
||||
|
||||
/// "Not supported by your provider."
|
||||
///
|
||||
/// Used in the connectivity view.
|
||||
#define DC_STR_NOT_SUPPORTED_BY_PROVIDER 113
|
||||
|
||||
/// "Messages"
|
||||
///
|
||||
/// Used as a subtitle in quota context; can be plural always.
|
||||
|
||||
@@ -275,7 +275,7 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.strdup()
|
||||
} else {
|
||||
match config::Config::from_str(&key)
|
||||
.with_context(|| format!("Invalid key {key:?}"))
|
||||
.with_context(|| format!("Invalid key {:?}", &key))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(key) => ctx
|
||||
@@ -528,7 +528,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::MsgFailed { .. } => 2012,
|
||||
EventType::MsgRead { .. } => 2015,
|
||||
EventType::MsgDeleted { .. } => 2016,
|
||||
EventType::MsgReadCountChanged { .. } => 2018,
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ChatDeleted { .. } => 2023,
|
||||
@@ -603,7 +602,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgFailed { chat_id, .. }
|
||||
| EventType::MsgRead { chat_id, .. }
|
||||
| EventType::MsgDeleted { chat_id, .. }
|
||||
| EventType::MsgReadCountChanged { chat_id, .. }
|
||||
| EventType::ChatModified(chat_id)
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. }
|
||||
| EventType::ChatDeleted { chat_id } => chat_id.to_u32() as libc::c_int,
|
||||
@@ -690,8 +688,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
| EventType::MsgRead { msg_id, .. }
|
||||
| EventType::MsgDeleted { msg_id, .. }
|
||||
| EventType::MsgReadCountChanged { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
| EventType::MsgDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::SecurejoinInviterProgress { progress, .. }
|
||||
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
@@ -765,7 +762,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::MsgFailed { .. }
|
||||
| EventType::MsgRead { .. }
|
||||
| EventType::MsgDeleted { .. }
|
||||
| EventType::MsgReadCountChanged { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ContactsChanged(_)
|
||||
| EventType::LocationChanged(_)
|
||||
|
||||
@@ -230,6 +230,7 @@ pub enum LotState {
|
||||
MsgInFresh = 10,
|
||||
MsgInNoticed = 13,
|
||||
MsgInSeen = 16,
|
||||
MsgOutPreparing = 18,
|
||||
MsgOutDraft = 19,
|
||||
MsgOutPending = 20,
|
||||
MsgOutFailed = 24,
|
||||
@@ -245,6 +246,7 @@ impl From<MessageState> for LotState {
|
||||
InFresh => LotState::MsgInFresh,
|
||||
InNoticed => LotState::MsgInNoticed,
|
||||
InSeen => LotState::MsgInSeen,
|
||||
OutPreparing => LotState::MsgOutPreparing,
|
||||
OutDraft => LotState::MsgOutDraft,
|
||||
OutPending => LotState::MsgOutPending,
|
||||
OutFailed => LotState::MsgOutFailed,
|
||||
|
||||
1
deltachat-jsonrpc-bindings/.gitignore
vendored
1
deltachat-jsonrpc-bindings/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
generated
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.54.0-dev"
|
||||
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[build-dependencies]
|
||||
deltachat-jsonrpc = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
vendored = ["deltachat-jsonrpc/vendored"]
|
||||
@@ -1,7 +0,0 @@
|
||||
use deltachat_jsonrpc::api::{write_qt_bindings, write_ts_bindings};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
write_ts_bindings(Path::new("typescript/generated"));
|
||||
write_qt_bindings(Path::new("qt/generated"), "deltachat");
|
||||
}
|
||||
1
deltachat-jsonrpc-bindings/qt/.gitignore
vendored
1
deltachat-jsonrpc-bindings/qt/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
generated
|
||||
@@ -1,104 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "deltachat-jsonrpc/generated/types.hpp"
|
||||
#include "deltachat-jsonrpc/generated/client.hpp"
|
||||
#include "deltachat.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace deltachat {
|
||||
|
||||
class CffiTransport : public Transport {
|
||||
public:
|
||||
explicit CffiTransport(dc_accounts_t* accounts)
|
||||
: jsonrpc_(dc_jsonrpc_init(accounts))
|
||||
{
|
||||
if (!jsonrpc_) std::abort();
|
||||
thread_ = std::thread([this] { run(); });
|
||||
}
|
||||
|
||||
virtual ~CffiTransport() {
|
||||
done_ = true;
|
||||
// Unblock dc_jsonrpc_next_response by sending a dummy request
|
||||
if (jsonrpc_) dc_jsonrpc_request(jsonrpc_, "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"get_system_info\"}");
|
||||
if (thread_.joinable()) thread_.join();
|
||||
std::lock_guard lk(mu_);
|
||||
for (auto& [id, prom] : pending_) {
|
||||
prom.set_value({{}, "Transport destructed", -32060});
|
||||
}
|
||||
pending_.clear();
|
||||
if (jsonrpc_) dc_jsonrpc_unref(jsonrpc_);
|
||||
}
|
||||
|
||||
virtual std::future<Result<QJsonValue>> send(const QString method, const QJsonValue params) override {
|
||||
uint32_t id = next_id_++;
|
||||
QJsonObject envelope{
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", static_cast<qint64>(id)},
|
||||
{"method", method},
|
||||
{"params", params},
|
||||
};
|
||||
|
||||
std::promise<Result<QJsonValue>> prom;
|
||||
std::future<Result<QJsonValue>> fut = prom.get_future();
|
||||
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
pending_[id] = std::move(prom);
|
||||
}
|
||||
|
||||
QByteArray json = QJsonDocument(envelope).toJson(QJsonDocument::Compact);
|
||||
dc_jsonrpc_request(jsonrpc_, json.constData());
|
||||
return fut;
|
||||
}
|
||||
private:
|
||||
void run() {
|
||||
while (!done_) {
|
||||
char* raw_json = dc_jsonrpc_next_response(jsonrpc_);
|
||||
if (!raw_json) {
|
||||
break;
|
||||
}
|
||||
QByteArray json{raw_json};
|
||||
dc_str_unref(raw_json);
|
||||
if (done_) break;
|
||||
|
||||
QJsonObject obj = QJsonDocument::fromJson(json).object();
|
||||
|
||||
if (!obj["id"].isDouble()) {
|
||||
qCritical() << "No valid rpc id in" << QString{json};
|
||||
continue;
|
||||
}
|
||||
uint32_t id = static_cast<uint32_t>(obj["id"].toInt());
|
||||
|
||||
std::promise<Result<QJsonValue>> prom;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
if (auto nh = pending_.extract(id)) {
|
||||
prom = std::move(nh.mapped());
|
||||
} else {
|
||||
qCritical() << "Could not map response" << QString{json};
|
||||
continue;
|
||||
}
|
||||
}
|
||||
prom.set_value(parseResult(obj));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
dc_jsonrpc_instance_t* jsonrpc_;
|
||||
std::thread thread_;
|
||||
std::mutex mu_;
|
||||
std::atomic<uint32_t> next_id_{1};
|
||||
std::atomic<bool> done_{false};
|
||||
std::unordered_map<uint32_t, std::promise<Result<QJsonValue>>> pending_;
|
||||
};
|
||||
|
||||
class CffiDeltaChat : public RawClient {
|
||||
public:
|
||||
explicit CffiDeltaChat(dc_accounts_t* accounts)
|
||||
: RawClient(std::make_unique<CffiTransport>(accounts)) {}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -157,7 +157,7 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(all_positional)]
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
impl CommandApi {
|
||||
/// Test function.
|
||||
async fn sleep(&self, delay: f64) {
|
||||
@@ -735,19 +735,10 @@ impl CommandApi {
|
||||
Ok(msg_ids)
|
||||
}
|
||||
|
||||
/// Estimates the number of messages that will be deleted
|
||||
/// by the `set_config()`-option `delete_device_after`.
|
||||
///
|
||||
/// Estimate the number of messages that will be deleted
|
||||
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
/// This is typically used to show the estimated impact to the user
|
||||
/// before actually enabling deletion of old messages.
|
||||
///
|
||||
/// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `from_server`: Deprecated, pass `false` here
|
||||
/// - `seconds`: Count messages older than the given number of seconds.
|
||||
///
|
||||
/// Returns the number of messages that are older than the given number of seconds.
|
||||
async fn estimate_auto_deletion_count(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1081,9 +1072,7 @@ impl CommandApi {
|
||||
/// Create a new unencrypted group chat.
|
||||
///
|
||||
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
||||
/// address-contacts. NB: Chats with similar names and the same members are merged on other
|
||||
/// devices, but usually users don't create such chats and look up the existing one instead, so
|
||||
/// chat split on the first device is acceptable.
|
||||
/// address-contacts.
|
||||
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_group_unencrypted(&ctx, &name)
|
||||
@@ -1108,6 +1097,9 @@ impl CommandApi {
|
||||
/// because the word "channel" already appears a lot in the code,
|
||||
/// which would make it hard to grep for it.
|
||||
///
|
||||
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||
/// see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
|
||||
///
|
||||
/// Returns the created chat's id.
|
||||
async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -203,17 +203,6 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Like [`EventType::MsgRead`], but also fires on subsequent MDNs,
|
||||
/// if there are multiple receivers, i.e. in groups and channels.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgReadCountChanged {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
|
||||
/// ID of the message that was read.
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message was deleted.
|
||||
///
|
||||
/// This event means that the message will no longer appear in the messagelist.
|
||||
@@ -557,10 +546,6 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::MsgReadCountChanged { chat_id, msg_id } => MsgReadCountChanged {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.54.0-dev"
|
||||
"version": "2.50.0-dev"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -122,7 +122,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
if name.ends_with(".eml") {
|
||||
let path_plus_name = format!("{real_spec}/{name}");
|
||||
let path_plus_name = format!("{}/{}", &real_spec, name);
|
||||
println!("Import: {path_plus_name}");
|
||||
if poke_eml_file(context, Path::new(&path_plus_name))
|
||||
.await
|
||||
@@ -133,11 +133,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Import: Cannot open directory {real_spec:?}.");
|
||||
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("Import: {read_cnt} items read from {real_spec:?}.");
|
||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||
if read_cnt > 0 {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
@@ -179,7 +179,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
if msg.has_location() { "📍" } else { "" },
|
||||
contact_name,
|
||||
&contact_name,
|
||||
contact_id,
|
||||
msgtext,
|
||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||
@@ -221,7 +221,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
},
|
||||
statestr,
|
||||
downloadstate,
|
||||
temp2,
|
||||
&temp2,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -561,7 +561,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.map_or_else(String::new, |prefix| format!("{prefix}: ")),
|
||||
summary.text,
|
||||
statestr,
|
||||
timestr,
|
||||
×tr,
|
||||
if chat.is_sending_locations() {
|
||||
"📍"
|
||||
} else {
|
||||
@@ -1236,9 +1236,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"estimatedeletion" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
||||
let seconds = arg1.parse()?;
|
||||
let from_server = false;
|
||||
let device_cnt = message::estimate_deletion_cnt(&context, from_server, seconds).await?;
|
||||
println!("estimated count of messages older than {seconds} seconds: {device_cnt}");
|
||||
let device_cnt = message::estimate_deletion_cnt(&context, false, seconds).await?;
|
||||
let server_cnt = message::estimate_deletion_cnt(&context, true, seconds).await?;
|
||||
println!(
|
||||
"estimated count of messages older than {seconds} seconds:\non device: {device_cnt}\non server: {server_cnt}"
|
||||
);
|
||||
}
|
||||
"" => (),
|
||||
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
|
||||
|
||||
@@ -432,7 +432,7 @@ async fn handle_cmd(
|
||||
{
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||
} else {
|
||||
println!("OAuth2 not available for {addr}.");
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
}
|
||||
} else {
|
||||
println!("oauth2: set addr first.");
|
||||
|
||||
@@ -29,7 +29,7 @@ $ pip install .
|
||||
|
||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
2. Install tox `pip install -U tox`
|
||||
3. Run `CHATMAIL_DOMAIN=ci-chatmail.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -340,6 +340,9 @@ class Account:
|
||||
because the word "channel" already appears a lot in the code,
|
||||
which would make it hard to grep for it.
|
||||
|
||||
After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||
see `create_group()` for more information on the unpromoted state.
|
||||
|
||||
Returns the created chat.
|
||||
"""
|
||||
return Chat(self, self._rpc.create_broadcast(self.id, name))
|
||||
|
||||
@@ -54,7 +54,6 @@ class EventType(str, Enum):
|
||||
MSG_DELIVERED = "MsgDelivered"
|
||||
MSG_FAILED = "MsgFailed"
|
||||
MSG_READ = "MsgRead"
|
||||
MSG_READ_COUNT_CHANGED = "MsgReadCountChanged"
|
||||
MSG_DELETED = "MsgDeleted"
|
||||
CHAT_MODIFIED = "ChatModified"
|
||||
CHAT_DELETED = "ChatDeleted"
|
||||
@@ -191,6 +190,7 @@ class MessageState(IntEnum):
|
||||
IN_FRESH = 10
|
||||
IN_NOTICED = 13
|
||||
IN_SEEN = 16
|
||||
OUT_PREPARING = 18
|
||||
OUT_DRAFT = 19
|
||||
OUT_PENDING = 20
|
||||
OUT_FAILED = 24
|
||||
|
||||
@@ -87,7 +87,6 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||
Test change status on chatlistitem when status changes failed
|
||||
"""
|
||||
(alice,) = acfactory.get_online_accounts(1)
|
||||
alice.set_config("force_encryption", "0")
|
||||
|
||||
invalid_contact = alice.create_contact("example@example.com", "invalid address")
|
||||
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.24.0"])
|
||||
def test_qr_setup_contact(acfactory, alice_and_remote_bob, version) -> None:
|
||||
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
|
||||
"""Test other-core Bob profile can do securejoin with Alice on current core."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||
|
||||
@@ -33,15 +33,6 @@ def test_qr_setup_contact(acfactory, alice_and_remote_bob, version) -> None:
|
||||
# Test that Bob verified Alice's profile.
|
||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||
|
||||
# Test that Bob can also scan a QR code
|
||||
# of Alice for which the key is not known yet.
|
||||
# For the test above Bob already knew the key from a vCard.
|
||||
alice2 = acfactory.get_online_account()
|
||||
qr_code = alice2.get_qr_code()
|
||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
|
||||
|
||||
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
"""Test other-core Bob profile can send a message to Alice on current core."""
|
||||
|
||||
@@ -14,13 +14,10 @@ def test_moved_markseen(acfactory, direct_imap, log):
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac2.bring_online()
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
log.section("ac2: creating DeltaChat folder")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
||||
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
|
||||
@@ -60,9 +57,11 @@ def test_moved_markseen(acfactory, direct_imap, log):
|
||||
def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac2.set_config("bcc_self", "1")
|
||||
for ac in ac1, ac2:
|
||||
ac.set_config("delete_server_after", "0")
|
||||
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2.wait_for_incoming_msg()
|
||||
@@ -82,18 +81,17 @@ def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||
ac1_direct_imap.select_folder("INBOX")
|
||||
ac2_direct_imap.select_folder("INBOX")
|
||||
|
||||
# Check that the mdn and original message is marked as seen
|
||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 2
|
||||
# Check that the mdn is marked as seen
|
||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||
# Check original message is marked as seen
|
||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||
|
||||
|
||||
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0")
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
@@ -4,29 +4,39 @@ from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import MessageState
|
||||
|
||||
|
||||
def test_bcc_self_is_enabled_when_setting_up_second_device(acfactory):
|
||||
def test_bcc_self_delete_server_after_defaults(acfactory):
|
||||
"""Test default values for bcc_self and delete_server_after."""
|
||||
ac = acfactory.get_online_account()
|
||||
|
||||
# Initially after getting online
|
||||
# the setting bcc_self is set to 0 because there is only one device
|
||||
# and delete_server_after is "1", meaning immediate deletion.
|
||||
assert ac.get_config("bcc_self") == "0"
|
||||
assert ac.get_config("delete_server_after") == "1"
|
||||
|
||||
# Setup a second device.
|
||||
ac_clone = ac.clone()
|
||||
ac_clone.bring_online()
|
||||
|
||||
# Second device setup enables bcc_self.
|
||||
# Second device setup
|
||||
# enables bcc_self and changes default delete_server_after.
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
assert ac_clone.get_config("bcc_self") == "1"
|
||||
assert ac.get_config("delete_server_after") == "0"
|
||||
|
||||
# Test manually disabling bcc_self
|
||||
assert ac_clone.get_config("bcc_self") == "1"
|
||||
assert ac_clone.get_config("delete_server_after") == "0"
|
||||
|
||||
# Manually disabling bcc_self
|
||||
# also restores the default for delete_server_after.
|
||||
ac.set_config("bcc_self", "0")
|
||||
assert ac.get_config("bcc_self") == "0"
|
||||
assert ac.get_config("delete_server_after") == "1"
|
||||
|
||||
# Cloning the account again enables bcc_self again
|
||||
# Cloning the account again enables bcc_self
|
||||
# even though it was manually disabled.
|
||||
ac_clone = ac.clone()
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
assert ac.get_config("delete_server_after") == "0"
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
@@ -37,14 +47,19 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
log.section("send out message without bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "0")
|
||||
chat = ac1.create_chat(ac2)
|
||||
self_addr = ac1.get_config("addr")
|
||||
other_addr = ac2.get_config("addr")
|
||||
|
||||
msg_out = chat.send_text("message1")
|
||||
assert not msg_out.get_snapshot().is_forwarded
|
||||
|
||||
# wait for send out (no BCC)
|
||||
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
assert self_addr not in ev.msg
|
||||
assert other_addr in ev.msg
|
||||
|
||||
log.section("ac1: setting bcc_self=1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
@@ -52,16 +67,20 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
# wait for send out (BCC)
|
||||
ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# Second client receives only the second message, but not the first.
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.msg
|
||||
assert self_addr in ev.msg
|
||||
|
||||
# BCC-self messages are marked as seen by the sender device.
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
|
||||
@@ -91,15 +91,7 @@ def test_lowercase_address(acfactory) -> None:
|
||||
assert account.list_transports()[0]["addr"] == addr
|
||||
|
||||
param = account.get_info()["used_transport_settings"]
|
||||
|
||||
domain = addr.rsplit("@")[-1]
|
||||
domain_upper = addr_upper.rsplit("@")[-1]
|
||||
assert domain in param
|
||||
assert domain_upper not in param
|
||||
|
||||
# Whole address should not appear in the info,
|
||||
# does not matter if uppercase or lowercase.
|
||||
assert addr not in param
|
||||
assert addr in param
|
||||
assert addr_upper not in param
|
||||
|
||||
|
||||
@@ -450,35 +442,6 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
assert chat_id == alice2_chat_bob.id
|
||||
|
||||
|
||||
def test_2nd_device_events_when_msgs_are_seen(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.start_io()
|
||||
|
||||
# Get an accepted chat, otherwise alice2 won't be notified about the 2nd message.
|
||||
chat_alice2 = alice2.create_chat(bob)
|
||||
chat_id_alice2 = chat_alice2.get_basic_snapshot().id
|
||||
|
||||
chat_bob_alice = bob.create_chat(alice)
|
||||
chat_bob_alice.send_text("Hello!")
|
||||
msg_alice = alice.wait_for_incoming_msg()
|
||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
||||
chat_bob_alice.send_text("What's new?")
|
||||
assert alice2.wait_for_incoming_msg_event().chat_id == chat_id_alice2
|
||||
chat_alice2 = alice2.get_chat_by_id(chat_id_alice2)
|
||||
assert chat_alice2.get_fresh_message_count() == 2
|
||||
|
||||
msg_alice.mark_seen()
|
||||
assert alice2.wait_for_msgs_changed_event().chat_id == chat_id_alice2
|
||||
assert chat_alice2.get_fresh_message_count() == 1
|
||||
|
||||
msg_id = alice.wait_for_msgs_changed_event().msg_id
|
||||
msg = alice.get_message_by_id(msg_id)
|
||||
msg.mark_seen()
|
||||
assert alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id == chat_id_alice2
|
||||
assert chat_alice2.get_fresh_message_count() == 0
|
||||
|
||||
|
||||
def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@@ -1128,7 +1091,6 @@ def test_rename_group(acfactory):
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
|
||||
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
||||
time.sleep(1)
|
||||
alice_group.set_name(name)
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
@@ -1270,12 +1232,10 @@ def test_leave_and_delete_group(acfactory, log):
|
||||
|
||||
|
||||
def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||
"""
|
||||
`bcc_self` is off by default,
|
||||
so that messages are supposed to be immediately autodeleted
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
|
||||
log.section("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.54.0-dev"
|
||||
"version": "2.50.0-dev"
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ for (const { folder_name, package_name } of platform_package_names) {
|
||||
|
||||
if (is_local) {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
|
||||
`file:${join(expected_cwd, "/../../deltachat-jsonrpc-bindings/typescript")}`;
|
||||
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
|
||||
} else {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
|
||||
}
|
||||
|
||||
13
deny.toml
13
deny.toml
@@ -18,11 +18,6 @@ ignore = [
|
||||
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||
"RUSTSEC-2025-0134",
|
||||
|
||||
# Unmaintained proc-macro-error2
|
||||
# Transitive dependency of typescript-type-def 0.5.13.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0173>
|
||||
"RUSTSEC-2026-0173",
|
||||
|
||||
# rustls-webpki v0.102.8
|
||||
# We cannot upgrade to >=0.103.10 because
|
||||
# it is a transitive dependency of iroh 0.35.0
|
||||
@@ -48,12 +43,7 @@ ignore = [
|
||||
# hickory-proto 0.25.2 quadratic complexity issue.
|
||||
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0119>
|
||||
"RUSTSEC-2026-0119",
|
||||
|
||||
# Timing side channel in ml-dsa dependency of rPGP.
|
||||
# We enable PQC for encryption rather than signatures.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2025-0144>
|
||||
"RUSTSEC-2025-0144",
|
||||
"RUSTSEC-2026-0119"
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -72,7 +62,6 @@ skip = [
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hybrid-array", version = "0.2.3" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "lru", version = "0.12.5" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
|
||||
150
flake.lock
generated
150
flake.lock
generated
@@ -3,19 +3,15 @@
|
||||
"android": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779918845,
|
||||
"narHash": "sha256-FbpOOBg15L7X6NWWmTKbSdccnH59Jq53wWmAO37d2Q8=",
|
||||
"lastModified": 1731356359,
|
||||
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "105c093afc8c8fbeea98f8e398403f93043eba17",
|
||||
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -32,11 +28,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768818222,
|
||||
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
|
||||
"lastModified": 1728330715,
|
||||
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
|
||||
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -47,17 +43,15 @@
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779876442,
|
||||
"narHash": "sha256-O25HomVNmdROO13PEQ3Ran8Hq5EsyLmVn8Gb8JvJtJE=",
|
||||
"lastModified": 1763361733,
|
||||
"narHash": "sha256-ka7dpwH3HIXCyD2wl5F7cPLeRbqZoY2ullALsvxdPt8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "2eff81fc84390a35e1565395ae945d9394856824",
|
||||
"rev": "6c8d48e3b0ae371b19ac1485744687b788e80193",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -71,11 +65,29 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -86,19 +98,14 @@
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"fenix": [
|
||||
"fenix"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780914171,
|
||||
"narHash": "sha256-NYoa+CvsCgayY356worC9g6QoZkKJmvrj6nVdEA2Lyk=",
|
||||
"lastModified": 1721727458,
|
||||
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "41b6e9efb62fac9c16d5617e0456a715928a2206",
|
||||
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -109,11 +116,11 @@
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1757882181,
|
||||
"narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
|
||||
"lastModified": 1730207686,
|
||||
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
|
||||
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -124,16 +131,60 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1779931091,
|
||||
"narHash": "sha256-gc8NEz7a++7OQPGvMv+zIjXCec1PO38XRXZRa3m97ew=",
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1762977756,
|
||||
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3052ddf0614791c1869384a868248be5607a309f",
|
||||
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "master",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 0,
|
||||
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
|
||||
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -142,20 +193,20 @@
|
||||
"inputs": {
|
||||
"android": "android",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"naersk": "naersk",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1779827300,
|
||||
"narHash": "sha256-J6pHxKoZzWCrAvOVInwBcYYWix/NWwM10Ad+i29Qc5s=",
|
||||
"lastModified": 1762860488,
|
||||
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "c3af07ad84d68adc5e652e86f0c20009caa29014",
|
||||
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -179,6 +230,21 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
94
flake.nix
94
flake.nix
@@ -2,16 +2,11 @@
|
||||
description = "Chatmail core";
|
||||
inputs = {
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
fenix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
naersk.inputs.nixpkgs.follows = "nixpkgs";
|
||||
naersk.inputs.fenix.follows = "fenix";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/master";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
android.url = "github:tadfisher/android-nixpkgs";
|
||||
android.inputs.nixpkgs.follows = "nixpkgs";
|
||||
android.inputs.flake-utils.follows = "flake-utils";
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
@@ -51,7 +46,6 @@
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
./deltachat-jsonrpc
|
||||
./deltachat-jsonrpc-bindings
|
||||
./deltachat-ratelimit
|
||||
./deltachat-repl
|
||||
./deltachat-rpc-client
|
||||
@@ -67,15 +61,37 @@
|
||||
];
|
||||
};
|
||||
|
||||
# Map from architecture name to nixpkgs targets.
|
||||
# Map from architecture name to rust targets and nixpkgs targets.
|
||||
arch2targets = {
|
||||
"x86_64-linux" = "x86_64-unknown-linux-musl";
|
||||
"armv7l-linux" = "armv7l-unknown-linux-musleabihf";
|
||||
"armv6l-linux" = "armv6l-unknown-linux-musleabihf";
|
||||
"aarch64-linux" = "aarch64-unknown-linux-musl";
|
||||
"i686-linux" = "i686-unknown-linux-musl";
|
||||
"x86_64-darwin" = "x86_64-darwin";
|
||||
"aarch64-darwin" = "aarch64-darwin";
|
||||
"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;
|
||||
@@ -92,10 +108,10 @@
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
};
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
mkWin64RustPackage = packageName:
|
||||
let
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
|
||||
rustTarget = "x86_64-pc-windows-gnu";
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
@@ -117,8 +133,6 @@
|
||||
];
|
||||
depsBuildBuild = [
|
||||
pkgsWin64.stdenv.cc
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
@@ -126,22 +140,21 @@
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
"-L"
|
||||
"native=${pkgsWin64.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
mkWin32RustPackage = packageName:
|
||||
let
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
|
||||
rustTarget = "i686-pc-windows-gnu";
|
||||
in
|
||||
let
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
@@ -167,8 +180,7 @@
|
||||
};
|
||||
})).overrideAttrs (oldAttr: {
|
||||
configureFlags = oldAttr.configureFlags ++ [
|
||||
"--disable-sjlj-exceptions"
|
||||
"--with-dwarf2"
|
||||
"--disable-sjlj-exceptions --with-dwarf2"
|
||||
];
|
||||
})
|
||||
);
|
||||
@@ -181,12 +193,9 @@
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.nasm # aws-lc-sys requires it
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
@@ -194,12 +203,9 @@
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
"-L"
|
||||
"native=${pkgsWin32.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
@@ -208,12 +214,14 @@
|
||||
|
||||
mkCrossRustPackage = arch: packageName:
|
||||
let
|
||||
crossTarget = arch2targets."${arch}";
|
||||
rustTarget = arch2targets."${arch}".rustTarget;
|
||||
crossTarget = arch2targets."${arch}".crossTarget;
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
|
||||
in
|
||||
let
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
@@ -496,6 +504,22 @@
|
||||
'';
|
||||
};
|
||||
|
||||
# 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";
|
||||
@@ -538,7 +562,7 @@
|
||||
deltachat-python
|
||||
deltachat-rpc-client
|
||||
pkgs.python3Packages.breathe
|
||||
pkgs.python3Packages.sphinx-rtd-theme
|
||||
pkgs.python3Packages.sphinx_rtd_theme
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.sphinx ];
|
||||
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.54.0-dev"
|
||||
version = "2.50.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -271,6 +271,15 @@ class Chat:
|
||||
sent out. This is the same object as was passed in, which
|
||||
has been modified with the new state of the core.
|
||||
"""
|
||||
if msg.is_out_preparing():
|
||||
assert msg.id != 0
|
||||
# get a fresh copy of dc_msg, the core needs it
|
||||
maybe_msg = Message.from_db(self.account, msg.id)
|
||||
if maybe_msg is not None:
|
||||
msg = maybe_msg
|
||||
else:
|
||||
raise ValueError("message does not exist")
|
||||
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
@@ -324,6 +333,26 @@ class Chat:
|
||||
raise ValueError("message could not be sent")
|
||||
return Message.from_db(self.account, sent_id)
|
||||
|
||||
def send_prepared(self, message):
|
||||
"""send a previously prepared message.
|
||||
|
||||
:param message: a :class:`Message` instance previously returned by
|
||||
:meth:`prepare_file`.
|
||||
:raises ValueError: if message can not be sent.
|
||||
:returns: a :class:`deltachat.message.Message` instance as sent out.
|
||||
"""
|
||||
assert message.id != 0 and message.is_out_preparing()
|
||||
# get a fresh copy of dc_msg, the core needs it
|
||||
msg = Message.from_db(self.account, message.id)
|
||||
|
||||
# pass 0 as chat-id because core-docs say it's ok when out-preparing
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
assert sent_id == msg.id
|
||||
# modify message in place to avoid bad state for the caller
|
||||
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
|
||||
|
||||
def set_draft(self, message):
|
||||
"""set message as draft.
|
||||
|
||||
|
||||
@@ -351,12 +351,17 @@ class Message:
|
||||
def is_outgoing(self):
|
||||
"""Return True if Message is outgoing."""
|
||||
return lib.dc_msg_get_state(self._dc_msg) in (
|
||||
const.DC_STATE_OUT_PREPARING,
|
||||
const.DC_STATE_OUT_PENDING,
|
||||
const.DC_STATE_OUT_FAILED,
|
||||
const.DC_STATE_OUT_MDN_RCVD,
|
||||
const.DC_STATE_OUT_DELIVERED,
|
||||
)
|
||||
|
||||
def is_out_preparing(self):
|
||||
"""Return True if Message is outgoing, but its file is being prepared."""
|
||||
return self._msgstate == const.DC_STATE_OUT_PREPARING
|
||||
|
||||
def is_out_pending(self):
|
||||
"""Return True if Message is outgoing, but is pending (no single checkmark)."""
|
||||
return self._msgstate == const.DC_STATE_OUT_PENDING
|
||||
|
||||
@@ -521,6 +521,7 @@ class ACFactory:
|
||||
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
configdict.setdefault("delete_server_after", 0)
|
||||
ac.update_config(configdict)
|
||||
self._acsetup._account2config[ac] = configdict
|
||||
self._preconfigure_key(ac)
|
||||
|
||||
@@ -298,6 +298,73 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert msg_in.text == msg_out.text
|
||||
|
||||
|
||||
def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
"""Test for the issue #4346:
|
||||
- User is added to a verified group.
|
||||
- First device of the user downloads "member added" from the group.
|
||||
- First device removes "member added" from the server.
|
||||
- Some new messages are sent to the group.
|
||||
- Second device comes online, receives these new messages.
|
||||
The result is an unverified group with unverified members.
|
||||
- First device re-gossips Autocrypt keys to the group.
|
||||
- Now the second device has all members and group verified.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
ac2.set_config("gossip_period", "0") # Re-gossip in every message
|
||||
acfactory.bring_accounts_online()
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
ac2.export_self_keys(str(dir))
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
# Wait for "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.is_system_message()
|
||||
|
||||
lp.sec("ac2: waiting for 'member added' to be deleted on the server")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
|
||||
lp.sec("ac1: sending 'hi' to the group")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
chat1.send_text("hi")
|
||||
|
||||
lp.sec("ac2_offl: going online, checking the 'hi' message")
|
||||
ac2_offl.start_io()
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text == "hi"
|
||||
ac2_offl_ac1_contact = msg_in.get_sender_contact()
|
||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
chat2_offl = msg_in.chat
|
||||
|
||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
||||
chat2.send_text("hi2")
|
||||
|
||||
lp.sec("ac2_offl: receiving message")
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text == "hi2"
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
def test_deleted_msgs_dont_reappear(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
@@ -15,9 +15,6 @@ def test_basic_imap_api(acfactory, tmp_path):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
imap2 = ac2.direct_imap
|
||||
|
||||
with imap2.idle() as idle2:
|
||||
@@ -165,9 +162,6 @@ def test_webxdc_message(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_webxdc()
|
||||
@@ -368,10 +362,6 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
# make DC's life harder wrt to encodings
|
||||
ac1.set_config("displayname", "ä name")
|
||||
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac2.set_config("bcc_self", "1")
|
||||
|
||||
# clear any fresh device messages
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
ac2.get_device_chat().mark_noticed()
|
||||
@@ -516,15 +506,9 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
ac1.set_config("mdns_enabled", "1")
|
||||
ac2.set_config("mdns_enabled", "1")
|
||||
|
||||
# Make sure that the mdn is not immediately auto-deleted on the server:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg_out = chat.send_text("message1")
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
lp.sec("disable ac1 MDNs")
|
||||
@@ -541,7 +525,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("ac1: waiting for incoming activity")
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
# Wait for the mdn to be marked as seen on IMAP.
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
@@ -1089,8 +1073,6 @@ def test_send_receive_locations(acfactory, lp):
|
||||
|
||||
def test_delete_multiple_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
# Make sure that messages are not immediately auto-deleted on the server:
|
||||
ac2.set_config("bcc_self", "1")
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: sending seven messages")
|
||||
|
||||
@@ -153,8 +153,7 @@ class TestOfflineContact:
|
||||
|
||||
def test_delete_referenced_contact_hides_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac2)
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
msg = contact1.create_chat().send_text("one message")
|
||||
assert ac1.delete_contact(contact1)
|
||||
assert not msg.filemime
|
||||
@@ -186,9 +185,8 @@ class TestOfflineChat:
|
||||
return acfactory.get_pseudo_configured_account()
|
||||
|
||||
@pytest.fixture()
|
||||
def chat1(self, ac1, acfactory):
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
return ac1.create_contact(ac2).create_chat()
|
||||
def chat1(self, ac1):
|
||||
return ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
|
||||
def test_display(self, chat1):
|
||||
str(chat1)
|
||||
@@ -406,7 +404,7 @@ class TestOfflineChat:
|
||||
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
|
||||
assert contact2.name == "real"
|
||||
|
||||
def test_send_lots_of_offline_msgs(self, acfactory, chat1):
|
||||
def test_send_lots_of_offline_msgs(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1.set_config("configured_mail_server", "example.org")
|
||||
ac1.set_config("configured_mail_user", "example.org")
|
||||
@@ -415,13 +413,13 @@ class TestOfflineChat:
|
||||
ac1.set_config("configured_send_user", "example.org")
|
||||
ac1.set_config("configured_send_pw", "example.org")
|
||||
ac1.start_io()
|
||||
chat = ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
for i in range(50):
|
||||
chat1.send_text("hello")
|
||||
chat.send_text("hello")
|
||||
|
||||
def test_create_chat_simple(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac2)
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact1.create_chat().send_text("hello")
|
||||
|
||||
def test_chat_message_distinctions(self, ac1, chat1):
|
||||
|
||||
@@ -152,8 +152,7 @@ def test_sig():
|
||||
|
||||
def test_markseen_invalid_message_ids(acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac2)
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
chat = contact1.create_chat()
|
||||
chat.send_text("one message")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-06-15
|
||||
2026-04-13
|
||||
@@ -1,9 +1,3 @@
|
||||
#!/bin/sh
|
||||
# Run clippy for all Rust code in the project.
|
||||
#
|
||||
# To check, run
|
||||
# scripts/clippy.sh
|
||||
#
|
||||
# To automatically fix warnings, run
|
||||
# scripts/clippy.sh --fix --allow-dirty
|
||||
cargo clippy --workspace --all-targets --all-features "$@" -- -D warnings
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
@@ -67,14 +67,13 @@ def main():
|
||||
parser.add_argument("newversion")
|
||||
|
||||
json_list = [
|
||||
"deltachat-jsonrpc-bindings/typescript/package.json",
|
||||
"deltachat-jsonrpc/typescript/package.json",
|
||||
"deltachat-rpc-server/npm-package/package.json",
|
||||
]
|
||||
toml_list = [
|
||||
"Cargo.toml",
|
||||
"deltachat-ffi/Cargo.toml",
|
||||
"deltachat-jsonrpc/Cargo.toml",
|
||||
"deltachat-jsonrpc-bindings/Cargo.toml",
|
||||
"deltachat-rpc-server/Cargo.toml",
|
||||
"deltachat-repl/Cargo.toml",
|
||||
"python/pyproject.toml",
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=2cba4b72f4c6e6417b83ba549aff7781be5f166c
|
||||
REV=ad097ee40579c884e7757de2d3bb0a51f481a32a
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
@@ -20,6 +20,86 @@ Description-Content-Type: text/markdown
|
||||
"""
|
||||
|
||||
|
||||
def build_source_package(version, filename):
|
||||
with tarfile.open(filename, "w:gz") as pkg:
|
||||
|
||||
def pack(name, contents):
|
||||
contents = contents.encode()
|
||||
tar_info = tarfile.TarInfo(f"deltachat_rpc_server-{version}/{name}")
|
||||
tar_info.mode = 0o644
|
||||
tar_info.size = len(contents)
|
||||
pkg.addfile(tar_info, BytesIO(contents))
|
||||
|
||||
pack("PKG-INFO", metadata_contents(version))
|
||||
pack(
|
||||
"pyproject.toml",
|
||||
f"""[build-system]
|
||||
requires = ["setuptools==68.2.2", "pip"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "{version}"
|
||||
|
||||
[project.scripts]
|
||||
deltachat-rpc-server = "deltachat_rpc_server:main"
|
||||
""",
|
||||
)
|
||||
pack(
|
||||
"setup.py",
|
||||
f"""
|
||||
import sys
|
||||
from setuptools import setup, find_packages
|
||||
from distutils.cmd import Command
|
||||
from setuptools.command.install import install
|
||||
from setuptools.command.build import build
|
||||
import subprocess
|
||||
import platform
|
||||
import tempfile
|
||||
from zipfile import ZipFile
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
|
||||
class BuildCommand(build):
|
||||
def run(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"download",
|
||||
"--no-input",
|
||||
"--timeout",
|
||||
"1000",
|
||||
"--platform",
|
||||
"musllinux_1_1_" + platform.machine(),
|
||||
"--only-binary=:all:",
|
||||
"deltachat-rpc-server=={version}",
|
||||
],
|
||||
cwd=tmpdir,
|
||||
)
|
||||
|
||||
wheel_path = next(Path(tmpdir).glob("*.whl"))
|
||||
with ZipFile(wheel_path, "r") as wheel:
|
||||
exe_path = wheel.extract("deltachat_rpc_server/deltachat-rpc-server", "src")
|
||||
Path(exe_path).chmod(0o700)
|
||||
wheel.extract("deltachat_rpc_server/__init__.py", "src")
|
||||
|
||||
shutil.rmtree(tmpdir)
|
||||
return super().run()
|
||||
|
||||
|
||||
setup(
|
||||
cmdclass={{"build": BuildCommand}},
|
||||
package_data={{"deltachat_rpc_server": ["deltachat-rpc-server"]}},
|
||||
)
|
||||
""",
|
||||
)
|
||||
pack("src/deltachat_rpc_server/__init__.py", "")
|
||||
|
||||
|
||||
def build_wheel(version, binary, tag, windows=False):
|
||||
filename = f"deltachat_rpc_server-{version}-{tag}.whl"
|
||||
|
||||
@@ -88,19 +168,23 @@ def main():
|
||||
with Path("Cargo.toml").open("rb") as fp:
|
||||
cargo_manifest = tomllib.load(fp)
|
||||
version = cargo_manifest["package"]["version"]
|
||||
arch = sys.argv[1]
|
||||
executable = sys.argv[2]
|
||||
tags = arch2tags[arch]
|
||||
|
||||
if arch in ["win32", "win64"]:
|
||||
build_wheel(
|
||||
version,
|
||||
executable,
|
||||
f"py3-none-{tags}",
|
||||
windows=True,
|
||||
)
|
||||
if sys.argv[1] == "source":
|
||||
filename = f"deltachat_rpc_server-{version}.tar.gz"
|
||||
build_source_package(version, filename)
|
||||
else:
|
||||
build_wheel(version, executable, f"py3-none-{tags}")
|
||||
arch = sys.argv[1]
|
||||
executable = sys.argv[2]
|
||||
tags = arch2tags[arch]
|
||||
|
||||
if arch in ["win32", "win64"]:
|
||||
build_wheel(
|
||||
version,
|
||||
executable,
|
||||
f"py3-none-{tags}",
|
||||
windows=True,
|
||||
)
|
||||
else:
|
||||
build_wheel(version, executable, f"py3-none-{tags}")
|
||||
|
||||
|
||||
main()
|
||||
|
||||
18
spec.md
18
spec.md
@@ -28,6 +28,7 @@ to implement typical messenger functions.
|
||||
- [Voice messages](#voice-messages)
|
||||
- [Reactions](#reactions)
|
||||
- [Attaching a contact to a message](#attaching-a-contact-to-a-message)
|
||||
- [Transitioning to a new e-mail address (AEAP)](#transitioning-to-a-new-e-mail-address-aeap)
|
||||
- [Miscellaneous](#miscellaneous)
|
||||
- [Sync messages](#sync-messages)
|
||||
|
||||
@@ -614,6 +615,23 @@ chatmail clients mark the gossiped key
|
||||
as indirectly verified.
|
||||
|
||||
|
||||
# Transitioning to a new e-mail address (AEAP)
|
||||
|
||||
When receiving a message:
|
||||
- If the key exists, but belongs to another address
|
||||
- AND there is a `Chat-Version` header
|
||||
- AND the message is signed correctly
|
||||
- AND the From address is (also) in the encrypted (and therefore signed) headers
|
||||
- AND the message timestamp is newer than the contact's `lastseen`
|
||||
(to prevent changing the address back when messages arrive out of order)
|
||||
(this condition is not that important
|
||||
since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups,
|
||||
possibly deduplicate the members list,
|
||||
and add a system message to all of these chats.
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
|
||||
@@ -794,7 +794,7 @@ impl Config {
|
||||
.with_push_subscriber(push_subscriber.clone())
|
||||
.build()
|
||||
.await
|
||||
.with_context(|| format!("failed to create context from file {dbfile:?}"))?;
|
||||
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
42
src/blob.rs
42
src/blob.rs
@@ -1,6 +1,6 @@
|
||||
//! # Blob directory management.
|
||||
|
||||
use std::cmp::{max, min};
|
||||
use std::cmp::max;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
@@ -12,7 +12,7 @@ use futures::StreamExt;
|
||||
use image::ImageReader;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use image::{codecs::jpeg::JpegEncoder, metadata::Orientation};
|
||||
use num_traits::{FromPrimitive, cast};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::{fs, task};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
|
||||
@@ -382,9 +382,14 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
img.apply_orientation(orientation);
|
||||
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit,
|
||||
// as set by `Config::MediaQuality`.
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit.
|
||||
// target_wh target-resolution for resizing the image.
|
||||
let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
|
||||
let mut target_wh = if exceeds_wh {
|
||||
max_wh
|
||||
} else {
|
||||
max(img.width(), img.height())
|
||||
};
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
@@ -423,35 +428,6 @@ impl<'a> BlobObject<'a> {
|
||||
});
|
||||
|
||||
if do_scale {
|
||||
let longest_side_len = max(img.width(), img.height());
|
||||
|
||||
// target_wh will be used as the target-resolution for resizing the image,
|
||||
// so that the longest sides of the image match the target-resolution.
|
||||
let mut target_wh = if !is_avatar {
|
||||
let area_sqrt = (f64::from(img.width()) * f64::from(img.height())).sqrt();
|
||||
// Limit resolution to the number of pixels that fit within max_wh * max_wh,
|
||||
// so that the image-quality does not depend on the aspect-ratio.
|
||||
let mut resolution_limit: u32 = cast(
|
||||
(f64::from(longest_side_len) * (f64::from(max_wh) / area_sqrt)).floor(),
|
||||
)
|
||||
.unwrap_or(max_wh);
|
||||
// Align at least one dimension of the resampled image to a multiple of 8 pixels,
|
||||
// to have fewer partially used JPEG-blocks (which represent 8x8 pixels each).
|
||||
if resolution_limit < longest_side_len && resolution_limit > 8 {
|
||||
while !resolution_limit.is_multiple_of(8) {
|
||||
resolution_limit -= 1
|
||||
}
|
||||
}
|
||||
resolution_limit
|
||||
} else {
|
||||
max_wh
|
||||
};
|
||||
|
||||
target_wh = min(target_wh, longest_side_len);
|
||||
|
||||
// For images in JPEG-format, 65535 pixels is the maximum resolution per dimension.
|
||||
target_wh = min(target_wh, 65535);
|
||||
|
||||
loop {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
|
||||
@@ -406,8 +406,8 @@ async fn test_recode_image_balanced_png() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 848,
|
||||
compressed_height: 477,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
@@ -497,8 +497,8 @@ async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 848,
|
||||
compressed_height: 477,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
@@ -517,8 +517,8 @@ async fn test_recode_image_huge_jpg() {
|
||||
has_exif: true,
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1704,
|
||||
compressed_height: 959,
|
||||
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
||||
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
struct CallSetup {
|
||||
@@ -635,23 +634,20 @@ async fn test_forward_call() -> Result<()> {
|
||||
async fn test_end_text_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let encrypted_message = test_utils::encrypt_raw_message(
|
||||
bob,
|
||||
&[alice],
|
||||
b"From: bob@example.net\r\n\
|
||||
To: alice@example.org\r\n\
|
||||
Message-ID: <first@example.net>\r\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
\r\n\
|
||||
Hello\r\n",
|
||||
let received1 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <first@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
\n\
|
||||
Hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let received1 = receive_imf(alice, encrypted_message.as_bytes(), false)
|
||||
.await?
|
||||
.unwrap();
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received1.msg_ids.len(), 1);
|
||||
let msg = Message::load_from_db(alice, received1.msg_ids[0])
|
||||
.await
|
||||
@@ -660,23 +656,21 @@ async fn test_end_text_call() -> Result<()> {
|
||||
|
||||
// Receiving "Call ended" message that refers
|
||||
// to the text message does not result in an error.
|
||||
let encrypted_message2 = test_utils::encrypt_raw_message(
|
||||
bob,
|
||||
&[alice],
|
||||
b"From: bob@example.net\r\n\
|
||||
To: alice@example.org\r\n\
|
||||
Message-ID: <second@example.net>\r\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\r\n\
|
||||
In-Reply-To: <first@example.net>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
Chat-Content: call-ended\r\n\
|
||||
\r\n\
|
||||
Call ended\r\n",
|
||||
let received2 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <second@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
In-Reply-To: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call-ended\n\
|
||||
\n\
|
||||
Call ended\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let received2 = receive_imf(alice, encrypted_message2.as_bytes(), false)
|
||||
.await?
|
||||
.unwrap();
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received2.msg_ids.len(), 1);
|
||||
assert_eq!(received2.chat_id, DC_CHAT_ID_TRASH);
|
||||
|
||||
|
||||
172
src/chat.rs
172
src/chat.rs
@@ -32,7 +32,6 @@ use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::{
|
||||
DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD,
|
||||
};
|
||||
use crate::ensure_and_debug_assert_eq;
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key::{Fingerprint, self_fingerprint};
|
||||
@@ -50,8 +49,8 @@ use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_secret, create_id,
|
||||
create_outgoing_rfc724_mid, get_abs_path, gm2local_offset, normalize_text, time,
|
||||
truncate_msg_text,
|
||||
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
|
||||
gm2local_offset, normalize_text, smeared_time, time, truncate_msg_text,
|
||||
};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
|
||||
@@ -292,7 +291,7 @@ impl ChatId {
|
||||
timestamp: i64,
|
||||
) -> Result<Self> {
|
||||
let grpname = sanitize_single_line(grpname);
|
||||
let timestamp = cmp::min(timestamp, time());
|
||||
let timestamp = cmp::min(timestamp, smeared_time(context));
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO chats (type, name, name_normalized, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, 0, ?)",
|
||||
@@ -1256,7 +1255,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
message_timestamp: i64,
|
||||
always_sort_to_bottom: bool,
|
||||
) -> Result<i64> {
|
||||
let mut sort_timestamp = cmp::min(message_timestamp, time());
|
||||
let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
|
||||
|
||||
let last_msg_time: Option<i64> = if always_sort_to_bottom {
|
||||
// get newest message for this chat
|
||||
@@ -1783,8 +1782,9 @@ impl Chat {
|
||||
);
|
||||
bail!("Cannot set message, contact for {} not found.", self.id);
|
||||
}
|
||||
} else if self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
ensure_and_debug_assert_eq!(self.typ, Chattype::Group,);
|
||||
} else if matches!(self.typ, Chattype::Group | Chattype::OutBroadcast)
|
||||
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
|
||||
{
|
||||
msg.param.set_int(Param::AttachChatAvatarAndDescription, 1);
|
||||
self.param
|
||||
.remove(Param::Unpromoted)
|
||||
@@ -2405,7 +2405,7 @@ impl ChatIdBlocked {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let now = time();
|
||||
let smeared_time = create_smeared_timestamp(context);
|
||||
|
||||
let chat_id = context
|
||||
.sql
|
||||
@@ -2420,7 +2420,7 @@ impl ChatIdBlocked {
|
||||
normalize_text(&chat_name),
|
||||
params.to_string(),
|
||||
create_blocked as u8,
|
||||
now,
|
||||
smeared_time,
|
||||
),
|
||||
)?;
|
||||
let chat_id = ChatId::new(
|
||||
@@ -2446,7 +2446,7 @@ impl ChatIdBlocked {
|
||||
&& !chat.param.exists(Param::Devicetalk)
|
||||
&& !chat.param.exists(Param::Selftalk)
|
||||
{
|
||||
chat_id.add_e2ee_notice(context, now).await?;
|
||||
chat_id.add_e2ee_notice(context, smeared_time).await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -2529,7 +2529,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
// running numbers, etc.
|
||||
let filename: String = match viewtype_orig {
|
||||
Viewtype::Voice => format!(
|
||||
"voice-messsage_{}.{suffix}",
|
||||
"voice-messsage_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
@@ -2537,9 +2537,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
|
||||
),
|
||||
&suffix
|
||||
),
|
||||
Viewtype::Image | Viewtype::Gif => format!(
|
||||
"image_{}.{suffix}",
|
||||
"image_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
@@ -2547,9 +2548,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
|
||||
),
|
||||
&suffix,
|
||||
),
|
||||
Viewtype::Video => format!(
|
||||
"video_{}.{suffix}",
|
||||
"video_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
@@ -2557,6 +2559,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
|
||||
),
|
||||
&suffix
|
||||
),
|
||||
_ => filename,
|
||||
};
|
||||
@@ -2610,7 +2613,7 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
|
||||
"chat_id cannot be a special chat: {chat_id}"
|
||||
);
|
||||
|
||||
if msg.state != MessageState::Undefined {
|
||||
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
// create_send_msg_jobs() will update `param` in the db.
|
||||
@@ -2718,7 +2721,10 @@ async fn prepare_send_msg(
|
||||
None
|
||||
};
|
||||
|
||||
if msg.state == MessageState::Undefined
|
||||
if matches!(
|
||||
msg.state,
|
||||
MessageState::Undefined | MessageState::OutPreparing
|
||||
)
|
||||
// Legacy SecureJoin "v*-request" messages are unencrypted.
|
||||
&& msg.param.get_cmd() != SystemMessage::SecurejoinMessage
|
||||
&& chat.is_encrypted(context).await?
|
||||
@@ -2730,7 +2736,7 @@ async fn prepare_send_msg(
|
||||
}
|
||||
msg.state = MessageState::OutPending;
|
||||
|
||||
msg.timestamp_sort = time();
|
||||
msg.timestamp_sort = create_smeared_timestamp(context);
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
if !msg.hidden {
|
||||
chat_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||
@@ -2823,12 +2829,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
.await?;
|
||||
}
|
||||
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default()
|
||||
|| (!msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
&& context.get_config_bool(Config::ForceEncryption).await?);
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
let mimefactory = match MimeFactory::from_msg(context, msg.clone()).await {
|
||||
Ok(mf) => mf,
|
||||
Err(err) => {
|
||||
@@ -2895,26 +2896,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
}
|
||||
|
||||
if needs_encryption && !rendered_msg.is_encrypted {
|
||||
let addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
let text = stock_str::unencrypted_email(
|
||||
/* unrecoverable */
|
||||
message::set_msg_failed(
|
||||
context,
|
||||
addr.unwrap_or_default()
|
||||
.split('@')
|
||||
.nth(1)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.await;
|
||||
message::set_msg_failed(context, msg, &text).await?;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
msg.chat_id,
|
||||
&text,
|
||||
SystemMessage::InvalidUnencryptedMail,
|
||||
Some(msg.timestamp_sort),
|
||||
msg.timestamp_sort,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
msg,
|
||||
"End-to-end-encryption unavailable unexpectedly.",
|
||||
)
|
||||
.await?;
|
||||
bail!(
|
||||
@@ -2924,7 +2910,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
);
|
||||
}
|
||||
|
||||
let now = time();
|
||||
let now = smeared_time(context);
|
||||
|
||||
if rendered_msg.last_added_location_id.is_some()
|
||||
&& let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await
|
||||
@@ -2951,8 +2937,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutDelivered` inclusive, except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26) AND
|
||||
-- From `InFresh` to `OutMdnRcvd` inclusive except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26,28) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
@@ -3046,7 +3032,7 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
save_text_edit_to_db(context, &mut original_msg, &new_text, &[]).await?;
|
||||
save_text_edit_to_db(context, &mut original_msg, &new_text).await?;
|
||||
|
||||
let mut edit_msg = Message::new_text(EDITED_PREFIX.to_owned() + &new_text); // prefix only set for nicer display in Non-Delta-MUAs
|
||||
edit_msg.set_quote(context, Some(&original_msg)).await?; // quote only set for nicer display in Non-Delta-MUAs
|
||||
@@ -3065,20 +3051,16 @@ pub(crate) async fn save_text_edit_to_db(
|
||||
context: &Context,
|
||||
original_msg: &mut Message,
|
||||
new_text: &str,
|
||||
mime_headers: &[u8],
|
||||
) -> Result<()> {
|
||||
original_msg.param.set_int(Param::IsEdited, 1);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET txt=?, txt_normalized=?, param=?, mime_headers=?, mime_modified=? WHERE id=?",
|
||||
"UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?",
|
||||
(
|
||||
new_text,
|
||||
normalize_text(new_text),
|
||||
original_msg.param.to_string(),
|
||||
mime_headers,
|
||||
!mime_headers.is_empty(),
|
||||
original_msg.id,
|
||||
),
|
||||
)
|
||||
@@ -3556,10 +3538,6 @@ pub async fn create_group_unencrypted(context: &Context, name: &str) -> Result<C
|
||||
/// unencrypted chats currently.
|
||||
/// * `grpid` - Group ID. Iff nonempty, the chat is encrypted (with key-contacts).
|
||||
/// * `name` - Chat name.
|
||||
///
|
||||
/// NB: Unencrypted chats with similar names and the same members are merged on other devices, but
|
||||
/// usually users don't create such chats and look up the existing one instead, so chat split on the
|
||||
/// first device is acceptable.
|
||||
pub(crate) async fn create_group_ex(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
@@ -3574,7 +3552,7 @@ pub(crate) async fn create_group_ex(
|
||||
chat_name = "…".to_string();
|
||||
}
|
||||
|
||||
let timestamp = time();
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
@@ -3634,6 +3612,9 @@ pub(crate) async fn create_group_ex(
|
||||
/// because the word "channel" already appears a lot in the code,
|
||||
/// which would make it hard to grep for it.
|
||||
///
|
||||
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||
/// see [`create_group`] for more information on the unpromoted state.
|
||||
///
|
||||
/// Returns the created chat's id.
|
||||
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
|
||||
let grpid = create_id();
|
||||
@@ -3657,7 +3638,7 @@ pub(crate) async fn create_out_broadcast_ex(
|
||||
bail!("Invalid broadcast channel name: {chat_name}.");
|
||||
}
|
||||
|
||||
let timestamp = time();
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| -> Result<ChatId> {
|
||||
let cnt: u32 = t.query_row(
|
||||
"SELECT COUNT(*) FROM chats WHERE grpid=?",
|
||||
@@ -3665,20 +3646,17 @@ pub(crate) async fn create_out_broadcast_ex(
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}");
|
||||
let mut params: Params = Params::new();
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time())?;
|
||||
|
||||
t.execute(
|
||||
"INSERT INTO chats
|
||||
(type, name, name_normalized, grpid, created_timestamp, param)
|
||||
VALUES(?, ?, ?, ?, ?, ?)",
|
||||
(type, name, name_normalized, grpid, created_timestamp)
|
||||
VALUES(?, ?, ?, ?, ?)",
|
||||
(
|
||||
Chattype::OutBroadcast,
|
||||
&chat_name,
|
||||
normalize_text(&chat_name),
|
||||
&grpid,
|
||||
timestamp,
|
||||
params.to_string(),
|
||||
),
|
||||
)?;
|
||||
let chat_id = ChatId::new(t.last_insert_rowid().try_into()?);
|
||||
@@ -3808,15 +3786,13 @@ pub(crate) async fn add_to_chat_contacts_table(
|
||||
|
||||
/// Removes a contact from the chat
|
||||
/// by updating the `remove_timestamp`.
|
||||
/// Returns whether the contact has been a chat member recently. If so, a removal message should be
|
||||
/// sent.
|
||||
pub(crate) async fn remove_from_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
) -> Result<()> {
|
||||
let now = time();
|
||||
let is_past_member = context
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats_contacts
|
||||
@@ -3824,15 +3800,12 @@ pub(crate) async fn remove_from_chat_contacts_table(
|
||||
WHERE chat_id=? AND contact_id=?",
|
||||
(now, chat_id, contact_id),
|
||||
)
|
||||
.await?
|
||||
> 0;
|
||||
Ok(is_past_member)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes a contact from the chat
|
||||
/// without leaving a trace in the db.
|
||||
/// Returns whether the contact was removed, even if it was a past contact. If so, a removal message
|
||||
/// should be sent if the removal is issued by this device.
|
||||
/// without leaving a trace.
|
||||
///
|
||||
/// Note that if we call this function,
|
||||
/// and then receive a message from another device
|
||||
@@ -3842,17 +3815,17 @@ pub(crate) async fn remove_from_chat_contacts_table_without_trace(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
let removed = context
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts
|
||||
WHERE chat_id=? AND contact_id=?",
|
||||
(chat_id, contact_id),
|
||||
)
|
||||
.await?
|
||||
> 0;
|
||||
Ok(removed)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a contact to the chat.
|
||||
@@ -3915,11 +3888,11 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
return Ok(false);
|
||||
}
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
let now = time();
|
||||
let smeared_time = smeared_time(context);
|
||||
chat.param
|
||||
.remove(Param::Unpromoted)
|
||||
.set_i64(Param::GroupNameTimestamp, now)
|
||||
.set_i64(Param::GroupDescriptionTimestamp, now);
|
||||
.set_i64(Param::GroupNameTimestamp, smeared_time)
|
||||
.set_i64(Param::GroupDescriptionTimestamp, smeared_time);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
if context.is_self_addr(contact.get_addr()).await? {
|
||||
@@ -4172,13 +4145,10 @@ pub async fn remove_contact_from_chat(
|
||||
|
||||
let mut sync = Nosync;
|
||||
|
||||
let removed = if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
|
||||
remove_from_chat_contacts_table(context, chat_id, contact_id).await?
|
||||
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
|
||||
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
} else {
|
||||
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?
|
||||
};
|
||||
if !removed {
|
||||
return Ok(());
|
||||
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?;
|
||||
}
|
||||
|
||||
// We do not return an error if the contact does not exist in the database.
|
||||
@@ -4485,6 +4455,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
}
|
||||
|
||||
/// Forwards multiple messages to a chat in another context.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn forward_msgs_2ctx(
|
||||
ctx_src: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
@@ -4495,6 +4466,7 @@ pub async fn forward_msgs_2ctx(
|
||||
ensure!(!chat_id.is_special(), "can not forward to special chat");
|
||||
|
||||
let mut created_msgs: Vec<MsgId> = Vec::new();
|
||||
let mut curr_timestamp: i64;
|
||||
|
||||
chat_id
|
||||
.unarchive_if_not_muted(ctx_dst, MessageState::Undefined)
|
||||
@@ -4503,7 +4475,7 @@ pub async fn forward_msgs_2ctx(
|
||||
if let Some(reason) = chat.why_cant_send(ctx_dst).await? {
|
||||
bail!("cannot send to {chat_id}: {reason}");
|
||||
}
|
||||
let now = time();
|
||||
curr_timestamp = create_smeared_timestamps(ctx_dst, msg_ids.len());
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for id in msg_ids {
|
||||
let ts: i64 = ctx_src
|
||||
@@ -4567,10 +4539,10 @@ pub async fn forward_msgs_2ctx(
|
||||
|
||||
msg.state = MessageState::OutPending;
|
||||
msg.rfc724_mid = create_outgoing_rfc724_mid();
|
||||
msg.pre_rfc724_mid.clear();
|
||||
msg.timestamp_sort = now;
|
||||
msg.timestamp_sort = curr_timestamp;
|
||||
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
|
||||
|
||||
curr_timestamp += 1;
|
||||
if !create_send_msg_jobs(ctx_dst, &mut msg).await?.is_empty() {
|
||||
ctx_dst.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
@@ -4663,7 +4635,7 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
} else {
|
||||
MessageState::InSeen
|
||||
},
|
||||
time(),
|
||||
create_smeared_timestamp(context),
|
||||
msg.param.to_string(),
|
||||
src_msg_id,
|
||||
src_msg_id,
|
||||
@@ -4840,7 +4812,7 @@ pub async fn add_device_msg_with_importance(
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let timestamp_sent = time();
|
||||
let timestamp_sent = create_smeared_timestamp(context);
|
||||
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
@@ -4987,7 +4959,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
} else {
|
||||
let sort_to_bottom = true;
|
||||
chat_id
|
||||
.calc_sort_timestamp(context, time(), sort_to_bottom)
|
||||
.calc_sort_timestamp(context, smeared_time(context), sort_to_bottom)
|
||||
.await?
|
||||
};
|
||||
|
||||
@@ -5150,7 +5122,7 @@ async fn set_contacts_by_fingerprints(
|
||||
Ok(broadcast_contacts_added)
|
||||
})
|
||||
.await?;
|
||||
let timestamp = time();
|
||||
let timestamp = smeared_time(context);
|
||||
for added_id in broadcast_contacts_added {
|
||||
let msg = stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED).await;
|
||||
add_info_msg_with_cmd(
|
||||
@@ -5234,11 +5206,9 @@ impl Context {
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
// Newly created chat will be soon unblocked, `Blocked::Yes` here is just
|
||||
// to hide it from chatlist, as at this point it is completely blank (no name etc.).
|
||||
// Even if app crashes at this point, the chat will re-appear on e.g. any new
|
||||
// message sent from other device.
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Yes)
|
||||
// Use `Request` so that even if the program crashes, the user doesn't have to look
|
||||
// into the blocked contacts.
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
|
||||
.await?
|
||||
.id
|
||||
}
|
||||
@@ -5262,13 +5232,7 @@ impl Context {
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
// Don't show a chat on other devices until securejoin completes.
|
||||
// E.g. pinning a not-synced chat on device A shouldn't display it on device B yet.
|
||||
//
|
||||
// A pinned chat will appear on other devices only when the first
|
||||
// message is sent from the device that read the invitation - which is the same
|
||||
// behavior as with un-pinned chats.
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Yes)
|
||||
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
|
||||
.await?
|
||||
.id
|
||||
}
|
||||
|
||||
@@ -3,18 +3,14 @@ use std::sync::Arc;
|
||||
use super::*;
|
||||
use crate::Event;
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::constants::{
|
||||
self, DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS, N_MSGS_TO_NEW_BROADCAST_MEMBER,
|
||||
};
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS, N_MSGS_TO_NEW_BROADCAST_MEMBER};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{ImexMode, has_backup, imex};
|
||||
use crate::message::{Message, MessengerMessage, delete_msgs};
|
||||
use crate::mimeparser::{self, MimeMessage};
|
||||
use crate::qr::{Qr, check_qr};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, sync,
|
||||
@@ -750,7 +746,7 @@ async fn test_leave_group() -> Result<()> {
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
assert_eq!(rcvd_leave_msg.state, MessageState::InNoticed);
|
||||
assert_eq!(rcvd_leave_msg.state, MessageState::InSeen);
|
||||
|
||||
alice.emit_event(EventType::Test);
|
||||
alice
|
||||
@@ -1163,51 +1159,46 @@ async fn test_archive() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unarchive_if_muted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let msg_from_bob = async |num: u32| {
|
||||
let encrypted_message = test_utils::encrypt_raw_message(
|
||||
bob,
|
||||
&[t],
|
||||
async fn msg_from_bob(t: &TestContext, num: u32) -> Result<()> {
|
||||
receive_imf(
|
||||
t,
|
||||
format!(
|
||||
"From: bob@example.net\r\n\
|
||||
To: alice@example.org\r\n\
|
||||
Message-ID: <{num}@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
Date: Sun, 22 Mar 2022 19:37:57 +0000\r\n\
|
||||
\r\n\
|
||||
hello\r\n"
|
||||
"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <{num}@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2022 19:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
receive_imf(t, encrypted_message.as_bytes(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
};
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
msg_from_bob(1).await;
|
||||
msg_from_bob(&t, 1).await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
chat_id.accept(t).await?;
|
||||
chat_id.set_visibility(t, ChatVisibility::Archived).await?;
|
||||
assert_eq!(get_archived_cnt(t).await?, 1);
|
||||
chat_id.accept(&t).await?;
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// not muted chat is unarchived on receiving a message
|
||||
msg_from_bob(2).await;
|
||||
assert_eq!(get_archived_cnt(t).await?, 0);
|
||||
msg_from_bob(&t, 2).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 0);
|
||||
|
||||
// forever muted chat is not unarchived on receiving a message
|
||||
chat_id.set_visibility(t, ChatVisibility::Archived).await?;
|
||||
set_muted(t, chat_id, MuteDuration::Forever).await?;
|
||||
msg_from_bob(3).await;
|
||||
assert_eq!(get_archived_cnt(t).await?, 1);
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
set_muted(&t, chat_id, MuteDuration::Forever).await?;
|
||||
msg_from_bob(&t, 3).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// otherwise muted chat is not unarchived on receiving a message
|
||||
set_muted(
|
||||
t,
|
||||
&t,
|
||||
chat_id,
|
||||
MuteDuration::Until(
|
||||
SystemTime::now()
|
||||
@@ -1216,12 +1207,12 @@ async fn test_unarchive_if_muted() -> Result<()> {
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
msg_from_bob(4).await;
|
||||
assert_eq!(get_archived_cnt(t).await?, 1);
|
||||
msg_from_bob(&t, 4).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// expired mute will unarchive the chat
|
||||
set_muted(
|
||||
t,
|
||||
&t,
|
||||
chat_id,
|
||||
MuteDuration::Until(
|
||||
SystemTime::now()
|
||||
@@ -1230,20 +1221,20 @@ async fn test_unarchive_if_muted() -> Result<()> {
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
msg_from_bob(5).await;
|
||||
assert_eq!(get_archived_cnt(t).await?, 0);
|
||||
msg_from_bob(&t, 5).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 0);
|
||||
|
||||
// no unarchiving on sending to muted chat or on adding info messages to muted chat
|
||||
chat_id.set_visibility(t, ChatVisibility::Archived).await?;
|
||||
set_muted(t, chat_id, MuteDuration::Forever).await?;
|
||||
send_text_msg(t, chat_id, "out".to_string()).await?;
|
||||
add_info_msg(t, chat_id, "info").await?;
|
||||
assert_eq!(get_archived_cnt(t).await?, 1);
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
set_muted(&t, chat_id, MuteDuration::Forever).await?;
|
||||
send_text_msg(&t, chat_id, "out".to_string()).await?;
|
||||
add_info_msg(&t, chat_id, "info").await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// finally, unarchive on sending to not muted chat
|
||||
set_muted(t, chat_id, MuteDuration::NotMuted).await?;
|
||||
send_text_msg(t, chat_id, "out2".to_string()).await?;
|
||||
assert_eq!(get_archived_cnt(t).await?, 0);
|
||||
set_muted(&t, chat_id, MuteDuration::NotMuted).await?;
|
||||
send_text_msg(&t, chat_id, "out2".to_string()).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1283,7 +1274,7 @@ async fn test_marknoticed_all_chats() -> Result<()> {
|
||||
|
||||
tcm.section("bob: receive messages, accept all chats and send a reply to each messsage");
|
||||
|
||||
while let Some(sent_msg) = alice.pop_sent_msg_opt().await {
|
||||
while let Some(sent_msg) = alice.pop_sent_msg_opt(Duration::default()).await {
|
||||
let bob_message = bob.recv_msg(&sent_msg).await;
|
||||
let bob_chat_id = bob_message.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
@@ -1291,7 +1282,7 @@ async fn test_marknoticed_all_chats() -> Result<()> {
|
||||
}
|
||||
|
||||
tcm.section("alice: receive replies from bob");
|
||||
while let Some(sent_msg) = bob.pop_sent_msg_opt().await {
|
||||
while let Some(sent_msg) = bob.pop_sent_msg_opt(Duration::default()).await {
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
}
|
||||
// ensure chats have unread messages
|
||||
@@ -1389,7 +1380,6 @@ async fn test_markfresh_chat() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archive_fresh_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
|
||||
async fn msg_from(t: &TestContext, name: &str, num: u32) -> Result<()> {
|
||||
receive_imf(
|
||||
@@ -1635,7 +1625,6 @@ async fn test_set_chat_name() {
|
||||
"another name",
|
||||
"something different",
|
||||
] {
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
set_chat_name(alice, chat_id, new_name).await.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
@@ -1884,38 +1873,45 @@ async fn test_lookup_self_by_contact_id() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_marknoticed_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
|
||||
|
||||
let bob_chat_id = bob.create_chat_id(alice).await;
|
||||
let sent = bob.send_text(bob_chat_id, "hello").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Fri, 23 Apr 2021 10:00:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chats = Chatlist::try_load(alice, 0, None, None).await?;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, chat.id);
|
||||
assert_eq!(chat.id.get_fresh_msg_cnt(alice).await?, 1);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 1);
|
||||
assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 1);
|
||||
assert_eq!(t.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
let msgs = get_chat_msgs(alice, chat.id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
let msg_id = match msgs.last().unwrap() {
|
||||
let msgs = get_chat_msgs(&t, chat.id).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = match msgs.first().unwrap() {
|
||||
ChatItem::Message { msg_id } => *msg_id,
|
||||
_ => MsgId::new_unset(),
|
||||
};
|
||||
let msg = message::Message::load_from_db(alice, msg_id).await?;
|
||||
let msg = message::Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
|
||||
marknoticed_chat(alice, chat.id).await?;
|
||||
marknoticed_chat(&t, chat.id).await?;
|
||||
|
||||
let chats = Chatlist::try_load(alice, 0, None, None).await?;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let msg = message::Message::load_from_db(alice, msg_id).await?;
|
||||
let msg = message::Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.state, MessageState::InNoticed);
|
||||
assert_eq!(chat.id.get_fresh_msg_cnt(alice).await?, 0);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 0);
|
||||
assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 0);
|
||||
assert_eq!(t.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1923,7 +1919,6 @@ async fn test_marknoticed_chat() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_request_fresh_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
@@ -1975,43 +1970,40 @@ async fn test_contact_request_fresh_messages() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_request_archive() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let bob_chat_id = bob.create_chat_id(alice).await;
|
||||
let bob_sent_text = bob.send_text(bob_chat_id, "hello").await;
|
||||
alice.recv_msg(&bob_sent_text).await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chats = Chatlist::try_load(alice, 0, None, None).await?;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert!(
|
||||
Chat::load_from_db(alice, chat_id)
|
||||
.await?
|
||||
.is_contact_request()
|
||||
);
|
||||
assert_eq!(get_archived_cnt(alice).await?, 0);
|
||||
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
|
||||
assert_eq!(get_archived_cnt(&t).await?, 0);
|
||||
|
||||
// archive request without accepting or blocking
|
||||
chat_id
|
||||
.set_visibility(alice, ChatVisibility::Archived)
|
||||
.await?;
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
|
||||
let chats = Chatlist::try_load(alice, 0, None, None).await?;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert!(chat_id.is_archived_link());
|
||||
assert_eq!(get_archived_cnt(alice).await?, 1);
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
|
||||
let chats = Chatlist::try_load(alice, DC_GCL_ARCHIVED_ONLY, None, None).await?;
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert!(
|
||||
Chat::load_from_db(alice, chat_id)
|
||||
.await?
|
||||
.is_contact_request()
|
||||
);
|
||||
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2019,7 +2011,6 @@ async fn test_contact_request_archive() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_classic_email_chat() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
|
||||
// Alice receives a classic (non-chat) message from Bob.
|
||||
receive_imf(
|
||||
@@ -2573,7 +2564,6 @@ async fn test_forward_encrypted_to_unencrypted() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
bob.allow_unencrypted().await?;
|
||||
|
||||
let txt = "This should be encrypted";
|
||||
let sent = alice.send_text(alice.create_chat(bob).await.id, txt).await;
|
||||
@@ -2803,30 +2793,6 @@ async fn test_can_send_group() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cant_remove_nonmember() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_broadcast_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_broadcast_id))
|
||||
.await
|
||||
.unwrap();
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
|
||||
let alice_charlie_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
remove_contact_from_chat(alice, alice_broadcast_id, alice_charlie_id).await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
assert!(!remove_from_chat_contacts_table(alice, alice_broadcast_id, alice_charlie_id).await?);
|
||||
assert!(
|
||||
!remove_from_chat_contacts_table_without_trace(alice, alice_broadcast_id, alice_charlie_id)
|
||||
.await?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that in a broadcast channel,
|
||||
/// the recipients can't see the identity of their fellow recipients.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -2950,30 +2916,15 @@ async fn test_broadcast_change_name() -> Result<()> {
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let broadcast_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let mut qr = get_securejoin_qr(alice, Some(broadcast_id)).await.unwrap();
|
||||
// Something goes wrong with the title, e.g. maybe it gets ellipsized
|
||||
// Note that the title always comes at the end for human readability
|
||||
qr += "+modified+title";
|
||||
|
||||
{
|
||||
tcm.section("Alice invites Bob to her channel");
|
||||
let Qr::AskJoinBroadcast { name, .. } = check_qr(bob, &qr).await? else {
|
||||
panic!();
|
||||
};
|
||||
assert_eq!(name, "Channel modified title");
|
||||
|
||||
// The channel's name gets fixed after actually joining the channel:
|
||||
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.name, "Channel");
|
||||
}
|
||||
let qr = get_securejoin_qr(alice, Some(broadcast_id)).await.unwrap();
|
||||
|
||||
tcm.section("Alice invites Bob to her channel");
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
tcm.section("Alice invites Fiona to her channel");
|
||||
tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||
|
||||
{
|
||||
tcm.section("Alice changes the chat name");
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
set_chat_name(alice, broadcast_id, "My great broadcast").await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -2992,7 +2943,6 @@ async fn test_broadcast_change_name() -> Result<()> {
|
||||
|
||||
{
|
||||
tcm.section("Alice changes the chat name again, but the system message is lost somehow");
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
set_chat_name(alice, broadcast_id, "Broadcast channel").await?;
|
||||
|
||||
let chat = Chat::load_from_db(alice, broadcast_id).await?;
|
||||
@@ -3069,11 +3019,13 @@ async fn test_broadcast_resend_to_new_member() -> Result<()> {
|
||||
}
|
||||
for i in 0..N_MSGS_TO_NEW_BROADCAST_MEMBER {
|
||||
let rev_order = false;
|
||||
let resent_msg = alice.pop_sent_msg_ex(rev_order).await.unwrap();
|
||||
let resent_msg = alice
|
||||
.pop_sent_msg_ex(rev_order, Duration::ZERO)
|
||||
.await
|
||||
.unwrap();
|
||||
let fiona_msg = fiona.recv_msg(&resent_msg).await;
|
||||
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
|
||||
assert_eq!(fiona_msg.text, (i + 1).to_string());
|
||||
assert_eq!(fiona_msg.param.get_bool(Param::WantsMdn).unwrap(), true);
|
||||
assert!(resent_msg.recipients.contains("fiona@example.net"));
|
||||
assert!(!resent_msg.recipients.contains("bob@"));
|
||||
// The message is undecryptable for Bob, he mustn't be able to know yet that somebody joined
|
||||
@@ -3087,32 +3039,7 @@ async fn test_broadcast_resend_to_new_member() -> Result<()> {
|
||||
);
|
||||
bob.recv_msg_trash(&resent_msg).await;
|
||||
}
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_resend_failed_msg_to_new_member() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_bc_id = create_broadcast(alice, "bc".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_bc_id)).await.unwrap();
|
||||
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let alice_msg_id = alice.send_text(alice_bc_id, "text").await.sender_msg_id;
|
||||
let mut msg = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
message::set_msg_failed(alice, &mut msg, "error").await?;
|
||||
let fiona_bc_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||
let resent_msg = alice.pop_sent_msg().await;
|
||||
let fiona_msg = fiona.recv_msg(&resent_msg).await;
|
||||
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
|
||||
assert_eq!(fiona_msg.text, "text");
|
||||
assert_eq!(
|
||||
alice_msg_id.get_state(alice).await?,
|
||||
MessageState::OutFailed
|
||||
);
|
||||
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3345,7 +3272,6 @@ async fn test_broadcasts_name_and_avatar() -> Result<()> {
|
||||
assert_eq!(bob_chat.get_profile_image(bob).await?, None);
|
||||
|
||||
tcm.section("Change broadcast channel name, and check that receivers see it");
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
set_chat_name(alice, alice_chat_id, "New Channel name").await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
@@ -3511,9 +3437,8 @@ async fn test_chat_description(
|
||||
"",
|
||||
"ä ẟ 😂",
|
||||
] {
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
tcm.section(&format!(
|
||||
"Alice sets the chat description to {description:?}"
|
||||
"Alice sets the chat description to '{description}'"
|
||||
));
|
||||
set_chat_description(alice, alice_chat_id, description).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
@@ -3552,7 +3477,12 @@ async fn test_chat_description(
|
||||
|
||||
tcm.section("Alice calls set_chat_description() without actually changing the description");
|
||||
set_chat_description(alice, alice_chat_id, "ä ẟ 😂").await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
assert!(
|
||||
alice
|
||||
.pop_sent_msg_opt(Duration::from_secs(0))
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3577,7 +3507,12 @@ async fn test_setting_empty_chat_description() -> Result<()> {
|
||||
let _hi = alice.send_text(alice_chat_id, "hi").await;
|
||||
|
||||
set_chat_description(alice, alice_chat_id, "").await?;
|
||||
assert!(alice.pop_sent_msg_opt().await.is_none());
|
||||
assert!(
|
||||
alice
|
||||
.pop_sent_msg_opt(Duration::from_secs(0))
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4524,9 +4459,7 @@ async fn test_get_chat_media_webxdc_order() -> Result<()> {
|
||||
assert_eq!(media.first().unwrap(), &instance1_id);
|
||||
assert_eq!(media.get(1).unwrap(), &instance2_id);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
|
||||
// add a status update for the other instance; that resorts the list
|
||||
// add a status update for the oder instance; that resorts the list
|
||||
alice
|
||||
.send_webxdc_status_update(instance1_id, r#"{"payload": {"foo": "bar"}}"#)
|
||||
.await?;
|
||||
@@ -4762,12 +4695,10 @@ async fn test_sync_delete_chat() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_adhoc_grp() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
a.allow_unencrypted().await?;
|
||||
}
|
||||
|
||||
let mut chat_ids = Vec::new();
|
||||
@@ -4942,6 +4873,10 @@ async fn test_sync_broadcast_and_send_message() -> Result<()> {
|
||||
vec![a2b_contact_id]
|
||||
);
|
||||
|
||||
// alice2's smeared clock may be behind alice1's one, so we need to work around "hi" appearing
|
||||
// before "You joined the channel." for bob. alice1 makes 3 more calls of
|
||||
// create_smeared_timestamp() than alice2 does as of 2026-03-10.
|
||||
SystemTime::shift(Duration::from_secs(3));
|
||||
tcm.section("Alice's second device sends a message to the channel");
|
||||
let sent_msg = alice2.send_text(a2_broadcast_id, "hi").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
@@ -5092,126 +5027,6 @@ async fn test_broadcast_contacts_are_hidden() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blocked_bob_cant_join_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
for a in [alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
// The observing device has Bob blocked from the early start.
|
||||
let alice2_bob_id = alice2.add_or_lookup_contact_id(bob).await;
|
||||
Contact::block(alice2, alice2_bob_id).await?;
|
||||
|
||||
let alice1_chat_id = create_group(alice1, "").await?;
|
||||
sync(alice1, alice2).await;
|
||||
let alice1_chat = Chat::load_from_db(alice1, alice1_chat_id).await?;
|
||||
let (alice2_chat_id, _blocked) = get_chat_id_by_grpid(alice2, &alice1_chat.grpid)
|
||||
.await?
|
||||
.unwrap();
|
||||
let qr = get_securejoin_qr(alice1, Some(alice1_chat_id)).await?;
|
||||
sync(alice1, alice2).await;
|
||||
|
||||
tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr)
|
||||
.await;
|
||||
let alice1_bob_id = alice1.add_or_lookup_contact_id(bob).await;
|
||||
assert_eq!(get_chat_contacts(alice1, alice1_chat_id).await?.len(), 2);
|
||||
// "vg-member-added" from alice1 adds bob for alice2 to provide membership consistency on
|
||||
// devices.
|
||||
assert_eq!(get_chat_contacts(alice2, alice2_chat_id).await?.len(), 2);
|
||||
remove_contact_from_chat(alice1, alice1_chat_id, alice1_bob_id).await?;
|
||||
bob.recv_msg(&alice1.pop_sent_msg().await).await;
|
||||
tcm.exec_securejoin_qr(bob, alice1, &qr).await;
|
||||
// Bob can join again if he isn't blocked.
|
||||
assert_eq!(get_chat_contacts(alice1, alice1_chat_id).await?.len(), 2);
|
||||
Contact::block(alice1, alice1_bob_id).await?;
|
||||
remove_contact_from_chat(alice1, alice1_chat_id, alice1_bob_id).await?;
|
||||
bob.recv_msg(&alice1.pop_sent_msg().await).await;
|
||||
tcm.exec_securejoin_qr(bob, alice1, &qr).await;
|
||||
let members = get_chat_contacts(alice1, alice1_chat_id).await?;
|
||||
assert_eq!(members.len(), 1);
|
||||
assert!(members.contains(&ContactId::SELF));
|
||||
let past_members = get_past_chat_contacts(alice1, alice1_chat_id).await?;
|
||||
assert_eq!(past_members.len(), 1);
|
||||
assert!(past_members.contains(&alice1_bob_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blocked_bob_cant_create_11_chat_via_securejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
for a in [alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
// The observing device has Bob blocked.
|
||||
let alice2_bob_id = alice2.add_or_lookup_contact_id(bob).await;
|
||||
Contact::block(alice2, alice2_bob_id).await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice1, None).await?;
|
||||
sync(alice1, alice2).await;
|
||||
|
||||
let chat_cnt = get_chat_cnt(alice1).await?;
|
||||
assert_eq!(get_chat_cnt(alice2).await?, chat_cnt);
|
||||
tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr)
|
||||
.await;
|
||||
assert_eq!(get_chat_cnt(alice1).await?, chat_cnt + 1);
|
||||
assert_eq!(get_chat_cnt(alice2).await?, chat_cnt);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test:
|
||||
///
|
||||
/// Pinning a chat that didn't complete securejoin on one device
|
||||
/// should not cause a blank chat to be displayed on other device.
|
||||
/// Such chat should be marked as blocked until the first message is sent.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_no_blank_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
for a in [alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
a.set_config_bool(Config::BccSelf, true).await?;
|
||||
}
|
||||
|
||||
let qr = get_securejoin_qr(bob, None).await?;
|
||||
|
||||
// Scan QR on alice1 and pin the resulting chat.
|
||||
let chat_id = tcm.exec_securejoin_qr(alice1, bob, &qr).await;
|
||||
chat_id
|
||||
.set_visibility(&alice1.ctx, ChatVisibility::Pinned)
|
||||
.await?;
|
||||
|
||||
// alice2 should receive a sync item about changing visibility of a chat it doesn't know about.
|
||||
sync(alice1, alice2).await;
|
||||
|
||||
// Chat created on alice2 should be blocked (hidden)
|
||||
let chat = Chat::load_from_db(&alice2.ctx, chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Yes);
|
||||
assert_eq!(chat.get_name(), "");
|
||||
assert_eq!(chat.visibility, ChatVisibility::Pinned);
|
||||
|
||||
// First message should unblock and fill missing data on other device.
|
||||
let msg = alice1.send_text(chat_id, "meow").await;
|
||||
|
||||
alice2.recv_msg(&msg).await;
|
||||
let chat = Chat::load_from_db(&alice2.ctx, chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Not);
|
||||
assert_eq!(chat.get_name(), "bob@example.net");
|
||||
assert_eq!(chat.visibility, ChatVisibility::Pinned);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests sending JPEG image with .png extension.
|
||||
///
|
||||
/// This is a regression test, previously sending failed
|
||||
@@ -5824,7 +5639,6 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_one_to_one_chat_no_group_member_timestamps() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await.unwrap();
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
||||
let sent = t.send_text(chat.id, "Hi!").await;
|
||||
let payload = sent.payload;
|
||||
@@ -5872,35 +5686,6 @@ async fn test_send_edit_request() -> Result<()> {
|
||||
let forwarded = alice2.get_last_msg().await;
|
||||
assert!(!forwarded.is_edited());
|
||||
|
||||
// If a message is too long after editing, it becomes an HTML message on the receiver side. On
|
||||
// the sender side it's still text so that it can be edited again.
|
||||
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
|
||||
static REPEAT_CNT: usize = constants::DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + 2;
|
||||
let long_txt = REPEAT_TXT.repeat(REPEAT_CNT);
|
||||
send_edit_request(alice, alice_msg.id, long_txt.clone()).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let test = Message::load_from_db(alice, alice_msg.id).await?;
|
||||
assert!(!test.has_html());
|
||||
assert_eq!(test.text, long_txt);
|
||||
bob.recv_msg_opt(&sent).await;
|
||||
let test = Message::load_from_db(bob, bob_msg.id).await?;
|
||||
assert!(test.is_edited());
|
||||
assert!(test.has_html());
|
||||
let html = test.id.get_html(bob).await?.unwrap();
|
||||
assert_eq!(html.matches("just repeated.<br/>").count(), REPEAT_CNT);
|
||||
assert!(test.text.matches("just repeated.").count() > 0);
|
||||
|
||||
// Alice shortens the message back so it's not HTML for Bob anymore.
|
||||
send_edit_request(alice, alice_msg.id, "Text me on Delta.Chat".to_string()).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let test = Message::load_from_db(alice, alice_msg.id).await?;
|
||||
assert_eq!(test.text, "Text me on Delta.Chat");
|
||||
bob.recv_msg_opt(&sent).await;
|
||||
let test = Message::load_from_db(bob, bob_msg.id).await?;
|
||||
assert!(test.is_edited());
|
||||
assert!(!test.has_html());
|
||||
assert_eq!(test.text, "Text me on Delta.Chat");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6067,7 +5852,6 @@ async fn test_send_delete_request_no_encryption() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let alice_chat = alice.create_email_chat(bob).await;
|
||||
|
||||
// Alice sends a message, then tries to send a deletion request which fails.
|
||||
@@ -6286,7 +6070,6 @@ async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
|
||||
let chat_id = receive_imf(
|
||||
alice,
|
||||
@@ -6322,7 +6105,6 @@ async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> {
|
||||
async fn test_create_unencrypted_group_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
|
||||
@@ -665,7 +665,6 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_single_chat() -> anyhow::Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
|
||||
// receive a one-to-one-message
|
||||
receive_imf(
|
||||
@@ -726,7 +725,6 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
|
||||
// receive a one-to-one-message without authname set
|
||||
receive_imf(
|
||||
|
||||
@@ -194,6 +194,17 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
/// 0 means messages are never deleted by Delta Chat.
|
||||
///
|
||||
/// Value 1 is treated as "delete at once": messages are deleted
|
||||
/// immediately, without moving to DeltaChat folder.
|
||||
///
|
||||
/// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise.
|
||||
DeleteServerAfter,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// device.
|
||||
///
|
||||
@@ -452,6 +463,11 @@ pub enum Config {
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
|
||||
/// Enables running test hooks, e.g. see `InnerContext::pre_encrypt_mime_hook`.
|
||||
/// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not
|
||||
/// using this still run unmodified code.
|
||||
TestHooks,
|
||||
|
||||
/// Return an error from `receive_imf_inner()`. For tests.
|
||||
SimulateReceiveImfError,
|
||||
|
||||
@@ -470,13 +486,6 @@ pub enum Config {
|
||||
/// Experimental option denoting that the current profile is shared between multiple team members.
|
||||
/// For now, the only effect of this option is that seen flags are not synchronized.
|
||||
TeamProfile,
|
||||
|
||||
/// Force encryption.
|
||||
///
|
||||
/// When enabled, unencrypted messages cannot be sent
|
||||
/// and incoming unencrypted messages are not fetched and not processed.
|
||||
#[strum(props(default = "1"))]
|
||||
ForceEncryption,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -492,11 +501,7 @@ impl Config {
|
||||
pub(crate) fn is_synced(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Displayname
|
||||
| Self::MdnsEnabled
|
||||
| Self::Selfavatar
|
||||
| Self::Selfstatus
|
||||
| Self::ForceEncryption,
|
||||
Self::Displayname | Self::MdnsEnabled | Self::Selfavatar | Self::Selfstatus,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -549,6 +554,14 @@ impl Context {
|
||||
// Default values
|
||||
let val = match key {
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
|
||||
Config::DeleteServerAfter => {
|
||||
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
|
||||
&& Box::pin(self.is_chatmail()).await?
|
||||
{
|
||||
true => Some("1".to_string()),
|
||||
false => Some("0".to_string()),
|
||||
}
|
||||
}
|
||||
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
|
||||
_ => key.get_str("default").map(|s| s.to_string()),
|
||||
};
|
||||
@@ -629,6 +642,23 @@ impl Context {
|
||||
self.get_config_bool(Config::MdnsEnabled).await
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
/// at once, `Some(x)` means delete after `x` seconds.
|
||||
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
|
||||
let val = match self
|
||||
.get_config_parsed::<i64>(Config::DeleteServerAfter)
|
||||
.await?
|
||||
.unwrap_or(0)
|
||||
{
|
||||
0 => None,
|
||||
1 => Some(0),
|
||||
x => Some(x),
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Gets the configured provider.
|
||||
///
|
||||
/// The provider is determined by the current primary transport.
|
||||
|
||||
@@ -142,6 +142,28 @@ async fn test_mdns_default_behaviour() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_server_after_default() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
|
||||
// Check that the settings are displayed correctly.
|
||||
assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
|
||||
assert_eq!(
|
||||
t.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
// Leaving emails on the server even w/o `BccSelf` is a good default at least because other
|
||||
// MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
|
||||
// does).
|
||||
t.set_config_bool(Config::BccSelf, false).await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -49,21 +49,6 @@ use crate::{chat, provider};
|
||||
/// see <https://github.com/chatmail/core/issues/7608>
|
||||
pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
|
||||
|
||||
/// Hard-coded candidates for default relays.
|
||||
/// In the future, we want to use it during onboarding;
|
||||
/// note that before onboarding automatically on any of these,
|
||||
/// we need to ask the admins whether their relay is able to handle this.
|
||||
/// For now, this is just the first 6 relays from chatmail.at/relays.
|
||||
#[allow(unused)]
|
||||
const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
|
||||
"mehl.cloud",
|
||||
"mailchat.pl",
|
||||
"chatmail.woodpeckersnest.space",
|
||||
"chatmail.culturanerd.it",
|
||||
"tarpit.fun",
|
||||
"d.gaufr.es",
|
||||
];
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
assert!(
|
||||
@@ -695,8 +680,6 @@ async fn get_autoconfig(
|
||||
param: &EnteredLoginParam,
|
||||
param_domain: &str,
|
||||
) -> Option<Vec<ServerParams>> {
|
||||
let accept_invalid_certificates = param.certificate_checks.accept_invalid_certificates();
|
||||
|
||||
// Make sure to not encode `.` as `%2E` here.
|
||||
// Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML
|
||||
// when address is encoded.
|
||||
@@ -713,7 +696,6 @@ async fn get_autoconfig(
|
||||
"https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
|
||||
),
|
||||
¶m.addr,
|
||||
accept_invalid_certificates,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -725,10 +707,10 @@ async fn get_autoconfig(
|
||||
ctx,
|
||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
|
||||
&format!(
|
||||
"https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
|
||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
||||
¶m_domain, ¶m_addr_urlencoded
|
||||
),
|
||||
¶m.addr,
|
||||
accept_invalid_certificates,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -739,8 +721,7 @@ async fn get_autoconfig(
|
||||
// Outlook uses always SSL but different domains (this comment describes the next two steps)
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
ctx,
|
||||
format!("https://{param_domain}/autodiscover/autodiscover.xml"),
|
||||
accept_invalid_certificates,
|
||||
format!("https://{}/autodiscover/autodiscover.xml", ¶m_domain),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -750,8 +731,10 @@ async fn get_autoconfig(
|
||||
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
ctx,
|
||||
format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",),
|
||||
accept_invalid_certificates,
|
||||
format!(
|
||||
"https://autodiscover.{}/autodiscover/autodiscover.xml",
|
||||
¶m_domain
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -762,9 +745,8 @@ async fn get_autoconfig(
|
||||
// always SSL for Thunderbird's database
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
&format!("https://autoconfig.thunderbird.net/v1.1/{param_domain}"),
|
||||
&format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
|
||||
¶m.addr,
|
||||
accept_invalid_certificates,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ use quick_xml::events::{BytesStart, Event};
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::net::read_url_with_tls;
|
||||
use crate::net::read_url;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -249,9 +249,8 @@ pub(crate) async fn moz_autoconfigure(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
addr: &str,
|
||||
accept_invalid_certificates: bool,
|
||||
) -> Result<Vec<ServerParams>, Error> {
|
||||
let xml_raw = read_url_with_tls(context, url, !accept_invalid_certificates).await?;
|
||||
let xml_raw = read_url(context, url).await?;
|
||||
|
||||
let res = parse_serverparams(addr, &xml_raw);
|
||||
if let Err(err) = &res {
|
||||
|
||||
@@ -10,7 +10,7 @@ use quick_xml::events::Event;
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::net::read_url_with_tls;
|
||||
use crate::net::read_url;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
/// Result of parsing a single `Protocol` tag.
|
||||
@@ -196,11 +196,10 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
|
||||
pub(crate) async fn outlk_autodiscover(
|
||||
context: &Context,
|
||||
mut url: String,
|
||||
accept_invalid_certificates: bool,
|
||||
) -> Result<Vec<ServerParams>, Error> {
|
||||
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
||||
for _i in 0..10 {
|
||||
let xml_raw = read_url_with_tls(context, &url, !accept_invalid_certificates).await?;
|
||||
let xml_raw = read_url(context, &url).await?;
|
||||
let res = parse_xml(&xml_raw);
|
||||
if let Err(err) = &res {
|
||||
warn!(context, "{}", err);
|
||||
|
||||
@@ -335,7 +335,6 @@ async fn test_add_or_lookup() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_name_changes() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
|
||||
// first message creates contact and one-to-one-chat without name set
|
||||
receive_imf(
|
||||
@@ -928,12 +927,9 @@ async fn test_synchronize_status() -> Result<()> {
|
||||
// Alice has two devices.
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
alice1.allow_unencrypted().await?;
|
||||
alice2.allow_unencrypted().await?;
|
||||
|
||||
// Bob has one device.
|
||||
let bob = &tcm.bob().await;
|
||||
bob.allow_unencrypted().await?;
|
||||
|
||||
let default_status = alice1.get_config(Config::Selfstatus).await?;
|
||||
|
||||
@@ -1002,18 +998,33 @@ async fn test_selfavatar_changed_event() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_last_seen() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let contact = alice.add_or_lookup_contact(bob).await;
|
||||
let (contact_id, _) = Contact::add_or_lookup(
|
||||
&alice,
|
||||
"Bob",
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), 0);
|
||||
|
||||
let msg = tcm.send_recv(bob, alice, "Hi.").await;
|
||||
let mime = br#"Subject: Hello
|
||||
Message-ID: message@example.net
|
||||
To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
Chat-Version: 1.0
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000
|
||||
|
||||
Hi."#;
|
||||
receive_imf(&alice, mime, false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let timestamp = msg.get_timestamp();
|
||||
assert!(timestamp > 0);
|
||||
let contact = Contact::get_by_id(alice, contact.id).await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), timestamp);
|
||||
|
||||
Ok(())
|
||||
@@ -1087,7 +1098,6 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
async fn test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat: bool) -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
bob.allow_unencrypted().await?;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
|
||||
assert!(std::str::from_utf8(raw)?.contains("Date: Thu, 24 Nov 2022 20:05:57 +0100"));
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::contact::{Contact, ContactId};
|
||||
use crate::debug_logging::DebugLogging;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{Imap, ServerMetadata};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::warn;
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
@@ -31,6 +32,7 @@ use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::{ConnectivityStore, SchedulerState};
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
use crate::tools::{self, duration_to_str, time, time_elapsed};
|
||||
use crate::transport::ConfiguredLoginParam;
|
||||
use crate::{chatlist_events, stats};
|
||||
@@ -226,6 +228,7 @@ pub struct InnerContext {
|
||||
/// Blob directory path
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) smeared_timestamp: SmearedTimestamp,
|
||||
/// The global "ongoing" process state.
|
||||
///
|
||||
/// This is a global mutex-like state for operations which should be modal in the
|
||||
@@ -330,6 +333,17 @@ pub struct InnerContext {
|
||||
/// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
|
||||
/// see [`Context::get_connectivity()`].
|
||||
pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
|
||||
|
||||
#[expect(clippy::type_complexity)]
|
||||
/// Transforms the root of the cryptographic payload before encryption.
|
||||
pub(crate) pre_encrypt_mime_hook: parking_lot::Mutex<
|
||||
Option<
|
||||
for<'a> fn(
|
||||
&Context,
|
||||
mail_builder::mime::MimePart<'a>,
|
||||
) -> mail_builder::mime::MimePart<'a>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -484,6 +498,7 @@ impl Context {
|
||||
blobdir,
|
||||
running_state: RwLock::new(Default::default()),
|
||||
sql: Sql::new(dbfile),
|
||||
smeared_timestamp: SmearedTimestamp::new(),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
housekeeping_mutex: Mutex::new(()),
|
||||
@@ -508,6 +523,7 @@ impl Context {
|
||||
self_fingerprint: OnceLock::new(),
|
||||
self_public_key: Mutex::new(None),
|
||||
connectivities: parking_lot::Mutex::new(Vec::new()),
|
||||
pre_encrypt_mime_hook: None.into(),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -827,6 +843,7 @@ impl Context {
|
||||
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let all_self_addrs = self.get_all_self_addrs().await?.join(", ");
|
||||
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -863,6 +880,10 @@ impl Context {
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM public_keys;", ())
|
||||
.await?;
|
||||
let fingerprint_str = match self_fingerprint(self).await {
|
||||
Ok(fp) => fp.to_string(),
|
||||
Err(err) => format!("<key failure: {err}>"),
|
||||
};
|
||||
|
||||
let mut res = get_info();
|
||||
|
||||
@@ -924,6 +945,7 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
res.insert("all_self_addrs", all_self_addrs);
|
||||
res.insert(
|
||||
"who_can_call_me",
|
||||
self.get_config_int(Config::WhoCanCallMe).await?.to_string(),
|
||||
@@ -940,6 +962,7 @@ impl Context {
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
res.insert(
|
||||
"media_quality",
|
||||
self.get_config_int(Config::MediaQuality).await?.to_string(),
|
||||
@@ -950,6 +973,12 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"delete_server_after",
|
||||
self.get_config_int(Config::DeleteServerAfter)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_housekeeping",
|
||||
self.get_config_int(Config::LastHousekeeping)
|
||||
@@ -1027,12 +1056,6 @@ impl Context {
|
||||
"team_profile",
|
||||
self.get_config_bool(Config::TeamProfile).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"force_encryption",
|
||||
self.get_config_bool(Config::ForceEncryption)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
|
||||
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -54,7 +54,6 @@ async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||
async fn test_get_fresh_msgs_and_muted_chats() {
|
||||
// receive various mails in 3 chats
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await.unwrap();
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
let claire = t.create_chat_with_contact("", "claire@g.it").await;
|
||||
let dave = t.create_chat_with_contact("", "dave@g.it").await;
|
||||
@@ -104,7 +103,6 @@ async fn test_get_fresh_msgs_and_muted_chats() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_until() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await.unwrap();
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
@@ -160,7 +158,6 @@ async fn test_get_fresh_msgs_and_muted_until() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_muted_context() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await?;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
t.set_config(Config::IsMuted, Some("1")).await?;
|
||||
let chat = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
@@ -327,7 +324,6 @@ async fn test_get_info_completeness() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
@@ -389,40 +385,49 @@ async fn test_search_msgs() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_unaccepted_requests() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("BobBar")).await?;
|
||||
let msg = tcm.send_recv(bob, t, "hello bob, foobar test!").await;
|
||||
let chat_id = msg.get_chat_id();
|
||||
let chat = Chat::load_from_db(t, chat_id).await?;
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: BobBar <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
||||
\n\
|
||||
hello bob, foobar test!\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Single);
|
||||
assert!(chat.is_contact_request());
|
||||
assert_eq!(Chatlist::try_load(t, 0, None, None).await?.len(), 1);
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(t, 0, Some("BobBar"), None).await?.len(),
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
chat_id.block(t).await?;
|
||||
chat_id.block(&t).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(t, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(t, 0, Some("BobBar"), None).await?.len(),
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
||||
|
||||
let contact_ids = get_chat_contacts(t, chat_id).await?;
|
||||
Contact::unblock(t, *contact_ids.first().unwrap()).await?;
|
||||
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
||||
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(t, 0, Some("BobBar"), None).await?.len(),
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
@@ -434,7 +439,6 @@ async fn test_search_unaccepted_requests() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_limit_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
@@ -402,7 +402,6 @@ mod tests {
|
||||
assert!(get_attachment_mime(&mail).is_some());
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.allow_unencrypted().await?;
|
||||
receive_imf(&bob, attachment_mime, false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
// Subject should be prepended because the attachment doesn't have "Chat-Version".
|
||||
@@ -417,7 +416,6 @@ mod tests {
|
||||
// Desktop via MS Exchange (actually made with TB though).
|
||||
let mixed_up_mime = include_bytes!("../test-data/message/mixed-up-long.eml");
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.allow_unencrypted().await?;
|
||||
receive_imf(&bob, mixed_up_mime, false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert!(!msg.get_text().is_empty());
|
||||
|
||||
@@ -6,16 +6,21 @@ use anyhow::{Result, anyhow, bail, ensure};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session;
|
||||
use crate::log::warn;
|
||||
use crate::message::{self, Message, MsgId, rfc724_mid_exists};
|
||||
use crate::{EventType, chatlist_events, ephemeral};
|
||||
use crate::{EventType, chatlist_events};
|
||||
|
||||
pub(crate) mod post_msg_metadata;
|
||||
pub(crate) use post_msg_metadata::PostMsgMetadata;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
/// the user might have no chance to actually download that message.
|
||||
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
||||
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
||||
|
||||
/// From this point onward outgoing messages are considered large
|
||||
/// and get a Pre-Message, which announces the Post-Message.
|
||||
/// This is only about sending so we can modify it any time.
|
||||
@@ -169,16 +174,6 @@ pub(crate) async fn download_msg(
|
||||
return Ok(None);
|
||||
}
|
||||
Box::pin(session.fetch_single_msg(context, &server_folder, server_uid, rfc724_mid)).await?;
|
||||
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
if ephemeral::should_delete_all_downloaded_messages(bcc_self, session.is_chatmail()) {
|
||||
// Now that the message was downloaded, it likely needs to be deleted;
|
||||
// trigger a re-check by interrupting the inbox folder.
|
||||
// This is mainly needed to make the tests pass;
|
||||
// on real devices, it would be fine to delete the message at the next iteration.
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
@@ -363,7 +358,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::chat::send_msg;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -383,14 +378,12 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_download_state() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id = t.create_chat_id(bob).await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
|
||||
|
||||
let mut msg = Message::new_text("Hi Bob".to_owned());
|
||||
let msg_id = send_msg(t, chat_id, &mut msg).await?;
|
||||
let msg = Message::load_from_db(t, msg_id).await?;
|
||||
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
|
||||
for s in &[
|
||||
@@ -400,15 +393,17 @@ mod tests {
|
||||
DownloadState::Done,
|
||||
DownloadState::Done,
|
||||
] {
|
||||
msg_id.update_download_state(t, *s).await?;
|
||||
let msg = Message::load_from_db(t, msg_id).await?;
|
||||
msg_id.update_download_state(&t, *s).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), *s);
|
||||
}
|
||||
t.sql
|
||||
.execute("DELETE FROM msgs WHERE id=?", (msg_id,))
|
||||
.await?;
|
||||
// Nothing to do is ok.
|
||||
msg_id.update_download_state(t, DownloadState::Done).await?;
|
||||
msg_id
|
||||
.update_download_state(&t, DownloadState::Done)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
42
src/e2ee.rs
42
src/e2ee.rs
@@ -42,25 +42,12 @@ impl EncryptHelper {
|
||||
compress: bool,
|
||||
seipd_version: SeipdVersion,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
let mut raw_message = Vec::new();
|
||||
let cursor = Cursor::new(&mut raw_message);
|
||||
mail_to_encrypt.clone().write_part(cursor).ok();
|
||||
|
||||
let ctext = self
|
||||
.encrypt_raw(context, keyring, raw_message, compress, seipd_version)
|
||||
.await?;
|
||||
Ok(ctext)
|
||||
}
|
||||
|
||||
pub async fn encrypt_raw(
|
||||
self,
|
||||
context: &Context,
|
||||
keyring: Vec<SignedPublicKey>,
|
||||
raw_message: Vec<u8>,
|
||||
compress: bool,
|
||||
seipd_version: SeipdVersion,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
let ctext =
|
||||
pgp::pk_encrypt(raw_message, keyring, sign_key, compress, seipd_version).await?;
|
||||
|
||||
@@ -109,11 +96,9 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat;
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::config::Config;
|
||||
use crate::message::Message;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
@@ -157,34 +142,11 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cannot_send_unencrypted_by_default() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat = alice.create_email_chat(bob).await;
|
||||
|
||||
let mut msg = Message::new_text("Hello!".to_string());
|
||||
assert!(chat::send_msg(alice, chat.id, &mut msg).await.is_err());
|
||||
assert_eq!(
|
||||
msg.error().unwrap(),
|
||||
"\u{26a0}\u{fe0f} Your email provider example.org requires end-to-end encryption which is not setup yet."
|
||||
);
|
||||
let info_msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
info_msg.get_info_type(),
|
||||
SystemMessage::InvalidUnencryptedMail
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::IsChatmail, true).await?;
|
||||
bob.allow_unencrypted().await?;
|
||||
let bob_chat_id = receive_imf(
|
||||
bob,
|
||||
b"From: alice@example.org\n\
|
||||
|
||||
139
src/ephemeral.rs
139
src/ephemeral.rs
@@ -23,15 +23,16 @@
|
||||
//! ## Device settings
|
||||
//!
|
||||
//! In addition to per-chat ephemeral message setting, each device has
|
||||
//! a global user-configured setting that complements per-chat
|
||||
//! settings, `delete_device_after`.
|
||||
//! This setting is not synchronized among devices and applies to all
|
||||
//! two global user-configured settings that complement per-chat
|
||||
//! settings: `delete_device_after` and `delete_server_after`. These
|
||||
//! settings are not synchronized among devices and apply to all
|
||||
//! messages known to the device, including messages sent or received
|
||||
//! before configuring the setting.
|
||||
//!
|
||||
//! `delete_device_after` configures the maximum time device is
|
||||
//! storing the messages locally,
|
||||
//! but does not delete messages from the server.
|
||||
//! storing the messages locally. `delete_server_after` configures the
|
||||
//! time after which device will delete the messages it knows about
|
||||
//! from the server.
|
||||
//!
|
||||
//! ## How messages are deleted
|
||||
//!
|
||||
@@ -59,8 +60,9 @@
|
||||
//!
|
||||
//! Server deletion happens by updating the `imap` table based on
|
||||
//! the database entries which are expired either according to their
|
||||
//! ephemeral message timers.
|
||||
//! ephemeral message timers or global `delete_server_after` setting.
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
@@ -73,11 +75,10 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::chat::{ChatId, ChatIdBlocked, send_msg};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::download::DownloadState;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
@@ -454,14 +455,11 @@ WHERE
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Locally deletes messages which are expired according to
|
||||
/// Deletes messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// Emits relevant `MsgsChanged` and `WebxdcInstanceDeleted` events
|
||||
/// if messages are deleted.
|
||||
///
|
||||
/// Also see [`delete_expired_imap_messages`],
|
||||
/// which marks the messages for deletion on the IMAP server.
|
||||
pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
|
||||
let rows = select_expired_messages(context, now).await?;
|
||||
|
||||
@@ -652,99 +650,42 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules expired IMAP messages for deletion on the server.
|
||||
///
|
||||
/// Also see [`delete_expired_messages`],
|
||||
/// which locally deletes expired messages.
|
||||
pub(crate) async fn delete_expired_imap_messages(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
is_chatmail: bool,
|
||||
) -> Result<()> {
|
||||
/// Schedules expired IMAP messages for deletion.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
|
||||
let now = time();
|
||||
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
if should_delete_all_downloaded_messages(bcc_self, is_chatmail) {
|
||||
// This is the only device using this relay.
|
||||
// Mark all downloaded messages for deletion, because they are not needed anymore.
|
||||
//
|
||||
// For pre- and post-messages, `rfc724_mid` contains the post-message's Message-Id.
|
||||
// The pre-message's Message-Id is in pre_rfc724_mid, if it exists.
|
||||
//
|
||||
// Pre-messages can be deleted even if the message wasn't fully downloaded yet,
|
||||
// because it's only the post-message that hasn't been downloaded.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR download_state=?3)
|
||||
AND id>9
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND id>9
|
||||
)",
|
||||
(transport_id, now, DownloadState::Done),
|
||||
)
|
||||
.await?;
|
||||
} else if bcc_self {
|
||||
// There may be other devices using this relay,
|
||||
// either because there is multi-device or because this is a classical email server.
|
||||
// Only delete expired ephemeral messages.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2 AND id>9
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2 AND id>9
|
||||
)",
|
||||
(transport_id, now),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// Single device.
|
||||
// Delete all expired and encrypted messages.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE id>9
|
||||
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
|
||||
((param GLOB '*\nc=1*' OR param GLOB 'c=1*') AND download_state=?3))
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND id>9
|
||||
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
|
||||
(param GLOB '*\nc=1*' OR param GLOB 'c=1*'))
|
||||
)",
|
||||
(transport_id, now, DownloadState::Done),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let (threshold_timestamp, threshold_timestamp_extended) =
|
||||
match context.get_config_delete_server_after().await? {
|
||||
None => (0, 0),
|
||||
Some(delete_server_after) => (
|
||||
match delete_server_after {
|
||||
// Guarantee immediate deletion.
|
||||
0 => i64::MAX,
|
||||
_ => now - delete_server_after,
|
||||
},
|
||||
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
||||
),
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ((download_state = 0 AND timestamp < ?) OR
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
)",
|
||||
(threshold_timestamp, threshold_timestamp_extended, now),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn should_delete_all_downloaded_messages(bcc_self: bool, is_chatmail: bool) -> bool {
|
||||
!bcc_self && is_chatmail
|
||||
}
|
||||
|
||||
/// Start ephemeral timers for seen messages if they are not started
|
||||
/// yet.
|
||||
///
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::download::DownloadState;
|
||||
use crate::location;
|
||||
use crate::message::markseen_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
||||
use crate::{
|
||||
chat::{self, Chat, ChatItem, create_group, send_text_msg},
|
||||
tools::IsNoneOrEmpty,
|
||||
@@ -171,7 +171,7 @@ async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt().await;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_none());
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
@@ -181,13 +181,13 @@ async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||
// Promote the group.
|
||||
send_text_msg(&alice, chat_id, "hi!".to_string()).await?;
|
||||
assert!(chat_id.is_promoted(&alice).await?);
|
||||
let sent = alice.pop_sent_msg_opt().await;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt().await;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
@@ -310,22 +310,20 @@ async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let self_chat = t.get_self_chat().await;
|
||||
|
||||
assert_eq!(next_expiration_timestamp(t).await, None);
|
||||
assert_eq!(next_expiration_timestamp(&t).await, None);
|
||||
|
||||
t.send_text(self_chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(self_chat.id).await;
|
||||
msg.id.trash(t, false).await?;
|
||||
check_msg_is_deleted(t, &self_chat, msg.id).await;
|
||||
msg.id.trash(&t, false).await?;
|
||||
check_msg_is_deleted(&t, &self_chat, msg.id).await;
|
||||
|
||||
self_chat
|
||||
.id
|
||||
.set_ephemeral_timer(t, Timer::Enabled { duration: 3600 })
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -333,7 +331,7 @@ async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -345,30 +343,38 @@ async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
|
||||
let bob_chat = t.create_chat(bob).await;
|
||||
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(t, msg.sender_msg_id, &bob_chat, now + 1799, time() + 1801)
|
||||
.await
|
||||
.unwrap();
|
||||
check_msg_will_be_deleted(
|
||||
&t,
|
||||
msg.sender_msg_id,
|
||||
&bob_chat,
|
||||
now + 1799,
|
||||
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
|
||||
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
|
||||
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
|
||||
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
|
||||
bob_chat
|
||||
.id
|
||||
.set_ephemeral_timer(t, Timer::Enabled { duration: 60 })
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -443,194 +449,107 @@ async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
let transport_id: u32 = 1;
|
||||
let uidvalidity = 12345u32;
|
||||
t.set_config_bool(Config::BccSelf, false).await?;
|
||||
|
||||
async fn is_deleted(context: &Context, mid: &str) -> Result<bool> {
|
||||
Ok(context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(mid,),
|
||||
let transport_id = 1;
|
||||
let uidvalidity = 12345;
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
(1010, now - 23 * HOUR, 0),
|
||||
(1020, now - 21 * HOUR, 0),
|
||||
(1030, now - 19 * HOUR, 0),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
(3000, now + HOUR, 0),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, target, uidvalidity) VALUES (?, ?,'INBOX',?, 'INBOX', ?);",
|
||||
(transport_id, &message_id, id, uidvalidity),
|
||||
)
|
||||
.await?
|
||||
== 1)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn reset_targets(context: &Context) {
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE imap SET target='INBOX'", ())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test messages:
|
||||
//
|
||||
// Four messages that were not split into pre- and post- message:
|
||||
// "expired@localhost" - expired ephemeral message
|
||||
// "no_expire@localhost" - non-ephemeral message
|
||||
// "no_expire_unencrypted@localhost" - non-ephemeral message, not encrypted
|
||||
// "future@localhost" - will expire in the future, but not yet
|
||||
//
|
||||
// And four messages that were split into pre- and post-message.
|
||||
// "expired_*@localhost" - has pre-msg, expired ephemeral message, not downloaded yet
|
||||
// "no_expire_*@localhost" - has pre-msg, non-ephemeral, not downloaded yet
|
||||
// "future_*@localhost" - has pre-msg, not expired yet, not downloaded yet
|
||||
// "done_*@localhost" - Fully downloaded -> post- message can be deleted
|
||||
//
|
||||
// The tuple is (rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid, is_encrypted)
|
||||
let msgs: [(&str, i64, DownloadState, &str, bool); 8] = [
|
||||
("expired@localhost", now - 1, DownloadState::Done, "", true),
|
||||
("no_expire@localhost", 0, DownloadState::Done, "", true),
|
||||
(
|
||||
"no_expire_unencrypted@localhost",
|
||||
0,
|
||||
DownloadState::Done,
|
||||
"",
|
||||
false,
|
||||
),
|
||||
// Use "now + 3600" rather than "now + 1", otherwise the test may be flaky
|
||||
// if it is slow and the message expires in a second
|
||||
(
|
||||
"future@localhost",
|
||||
now + 3600,
|
||||
DownloadState::Done,
|
||||
"",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"expired_post@localhost",
|
||||
now - 1,
|
||||
DownloadState::Available,
|
||||
"expired_pre@localhost",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"no_expire_post@localhost",
|
||||
0,
|
||||
DownloadState::Available,
|
||||
"no_expire_pre@localhost",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"future_post@localhost",
|
||||
now + 3600,
|
||||
DownloadState::Available,
|
||||
"future_pre@localhost",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"done_post@localhost",
|
||||
0,
|
||||
DownloadState::Done,
|
||||
"done_pre@localhost",
|
||||
true,
|
||||
),
|
||||
];
|
||||
for (rfc724_mid, ephemeral_timestamp, download_state, pre_rfc724_mid, is_encrypted) in msgs {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs \
|
||||
(rfc724_mid, timestamp, ephemeral_timestamp, download_state, pre_rfc724_mid, param) \
|
||||
VALUES (?,?,?,?,?,?)",
|
||||
(
|
||||
rfc724_mid,
|
||||
now,
|
||||
ephemeral_timestamp,
|
||||
download_state,
|
||||
pre_rfc724_mid,
|
||||
if is_encrypted {
|
||||
"c=1"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
)
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
let rfc724_mids: Vec<&str> = msgs
|
||||
.iter()
|
||||
.flat_map(|(rfc724_mid, _, _, pre_rfc724_mid, _is_encrypted)| {
|
||||
[*rfc724_mid, *pre_rfc724_mid]
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
for (i, rfc724_mid) in rfc724_mids.iter().enumerate() {
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap \
|
||||
(transport_id, rfc724_mid, folder, uid, target, uidvalidity) \
|
||||
VALUES (?,?,'INBOX',?,'INBOX',?)",
|
||||
(transport_id, *rfc724_mid, (i + 1) as u32, uidvalidity),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
for (is_chatmail, other_transport, bcc_self) in [
|
||||
(false, false, false),
|
||||
(false, false, true),
|
||||
(false, true, false),
|
||||
(false, true, true),
|
||||
(true, false, false),
|
||||
(true, false, true),
|
||||
(true, true, false),
|
||||
(true, true, true),
|
||||
] {
|
||||
println!(
|
||||
"Testing combination is_chatmail={is_chatmail}, other_transport={other_transport}, bcc_self={bcc_self}"
|
||||
);
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
|
||||
t.set_config_bool(Config::BccSelf, bcc_self).await?;
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
|
||||
delete_expired_imap_messages(
|
||||
&t,
|
||||
if other_transport {
|
||||
transport_id + 1
|
||||
} else {
|
||||
transport_id
|
||||
},
|
||||
is_chatmail,
|
||||
)
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
||||
.await?;
|
||||
|
||||
if other_transport {
|
||||
// Nothing should be deleted on another transport
|
||||
for rfc724_mid in &rfc724_mids {
|
||||
assert_eq!(is_deleted(&t, rfc724_mid).await?, false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
assert_eq!(is_deleted(&t, "expired@localhost").await?, true);
|
||||
assert_eq!(is_deleted(&t, "no_expire@localhost").await?, !bcc_self);
|
||||
assert_eq!(
|
||||
is_deleted(&t, "no_expire_unencrypted@localhost").await?,
|
||||
is_chatmail && !bcc_self
|
||||
);
|
||||
assert_eq!(is_deleted(&t, "future@localhost").await?, !bcc_self);
|
||||
assert_eq!(is_deleted(&t, "expired_post@localhost").await?, true);
|
||||
assert_eq!(is_deleted(&t, "expired_pre@localhost").await?, true);
|
||||
assert_eq!(is_deleted(&t, "no_expire_post@localhost").await?, false);
|
||||
assert_eq!(is_deleted(&t, "no_expire_pre@localhost").await?, !bcc_self);
|
||||
assert_eq!(is_deleted(&t, "future_post@localhost").await?, false);
|
||||
assert_eq!(is_deleted(&t, "future_pre@localhost").await?, !bcc_self);
|
||||
assert_eq!(is_deleted(&t, "done_pre@localhost").await?, !bcc_self);
|
||||
assert_eq!(is_deleted(&t, "done_post@localhost").await?, !bcc_self);
|
||||
|
||||
reset_targets(&t).await;
|
||||
}
|
||||
|
||||
// With BccSelf=true, non-expired messages are kept even if `is_chatmail` is true
|
||||
t.set_config_bool(Config::BccSelf, true).await?;
|
||||
delete_expired_imap_messages(&t, transport_id, true).await?;
|
||||
assert_eq!(is_deleted(&t, "expired@localhost").await?, true);
|
||||
assert_eq!(is_deleted(&t, "no_expire@localhost").await?, false);
|
||||
assert_eq!(is_deleted(&t, "done_pre@localhost").await?, false);
|
||||
assert_eq!(is_deleted(&t, "done_post@localhost").await?, false);
|
||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 3000).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -638,54 +557,50 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
// Regression test for a bug in the timer rollback protection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Message with Message-ID <first@example.com> and no timer is received.
|
||||
let encrypted_msg = test_utils::encrypt_raw_message(
|
||||
bob,
|
||||
&[alice],
|
||||
b"From: Bob <bob@example.net>\r\n\
|
||||
To: Alice <alice@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
Subject: Subject\r\n\
|
||||
Message-ID: <first@example.com>\r\n\
|
||||
Date: Sun, 22 Mar 2020 00:10:00 +0000\r\n\
|
||||
\r\n\
|
||||
hello\r\n",
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
receive_imf(alice, encrypted_msg.as_bytes(), false).await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(chat_id.get_ephemeral_timer(alice).await?, Timer::Disabled);
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
// Message with Message-ID <second@example.com> is received.
|
||||
let encrypted_msg = test_utils::encrypt_raw_message(
|
||||
bob,
|
||||
&[alice],
|
||||
b"From: Bob <bob@example.net>\r\n\
|
||||
To: Alice <alice@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
Subject: Subject\r\n\
|
||||
Message-ID: <second@example.com>\r\n\
|
||||
Date: Sun, 22 Mar 2020 00:11:00 +0000\r\n\
|
||||
Ephemeral-Timer: 60\r\n\
|
||||
\r\n\
|
||||
second message\r\n",
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
|
||||
Ephemeral-Timer: 60\n\
|
||||
\n\
|
||||
second message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
receive_imf(alice, encrypted_msg.as_bytes(), false).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(alice).await?,
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
// Message is deleted when its timer expires.
|
||||
msg.id.trash(alice, false).await?;
|
||||
msg.id.trash(&alice, false).await?;
|
||||
|
||||
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
@@ -699,26 +614,25 @@ async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
//
|
||||
// The message also contains a quote of the first message to test that only References:
|
||||
// header and not In-Reply-To: is consulted by the rollback protection.
|
||||
let encrypted_msg = test_utils::encrypt_raw_message(
|
||||
bob,
|
||||
&[alice],
|
||||
b"From: Bob <bob@example.net>\r\n\
|
||||
To: Alice <alice@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
Subject: Subject\r\n\
|
||||
Message-ID: <third@example.com>\r\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\r\n\
|
||||
References: <first@example.com> <second@example.com>\r\n\
|
||||
In-Reply-To: <first@example.com>\r\n\
|
||||
\r\n\
|
||||
> hello\r\n",
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||
References: <first@example.com> <second@example.com>\n\
|
||||
In-Reply-To: <first@example.com>\n\
|
||||
\n\
|
||||
> hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
receive_imf(alice, encrypted_msg.as_bytes(), false).await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
msg.chat_id.get_ephemeral_timer(alice).await?,
|
||||
msg.chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
@@ -730,20 +644,24 @@ async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
// successful reconnection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(alice, chat.id, &mut msg).await.is_err());
|
||||
assert!(
|
||||
chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
let now = time();
|
||||
check_msg_will_be_deleted(alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?;
|
||||
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?;
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -521,7 +521,6 @@ mod test_chatlist_events {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let mime = br#"Subject: First thread
|
||||
Message-ID: first@example.org
|
||||
To: Alice <alice@example.org>, Bob <bob@example.net>
|
||||
|
||||
@@ -181,17 +181,6 @@ pub enum EventType {
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Like [`EventType::MsgRead`], but also fires on subsequent MDNs,
|
||||
/// if there are multiple receivers, i.e. in groups and channels.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgReadCountChanged {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the message that was read.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// A single message was deleted.
|
||||
///
|
||||
/// This event means that the message will no longer appear in the messagelist.
|
||||
|
||||
120
src/html.rs
120
src/html.rs
@@ -9,7 +9,7 @@
|
||||
|
||||
use std::mem;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use anyhow::{Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use mailparse::ParsedContentType;
|
||||
use mime::Mime;
|
||||
@@ -17,12 +17,10 @@ use mime::Mime;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::message::{self, Message, MsgId};
|
||||
use crate::mimeparser::parse_message_id;
|
||||
use crate::param::{Param::SendHtml, Params};
|
||||
use crate::param::Param::SendHtml;
|
||||
use crate::plaintext::PlainText;
|
||||
use crate::sql;
|
||||
use crate::tools::{buf_compress, buf_decompress};
|
||||
|
||||
impl Message {
|
||||
/// Check if the message can be retrieved as HTML.
|
||||
@@ -260,71 +258,28 @@ impl MsgId {
|
||||
/// NB: we do not save raw mime unconditionally in the database to save space.
|
||||
/// The corresponding ffi-function is `dc_get_msg_html()`.
|
||||
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
|
||||
let (param, headers, compressed) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT param, mime_headers, mime_compressed FROM msgs WHERE id=?",
|
||||
(self,),
|
||||
|row| {
|
||||
let param: String = row.get(0)?;
|
||||
let param: Params = param.parse().unwrap_or_default();
|
||||
let headers = sql::row_get_vec(row, 1)?;
|
||||
let compressed: bool = row.get(2)?;
|
||||
Ok((param, headers, compressed))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if let Some(html) = param.get(SendHtml) {
|
||||
// If there are many concurrent db readers, going to the queue earlier makes sense.
|
||||
let (param, rawmime) = tokio::join!(
|
||||
self.get_param(context),
|
||||
message::get_mime_headers(context, self)
|
||||
);
|
||||
if let Some(html) = param?.get(SendHtml) {
|
||||
return Ok(Some(html.to_string()));
|
||||
}
|
||||
let from_rawmime = |rawmime: Vec<u8>| {
|
||||
if !rawmime.is_empty() {
|
||||
match HtmlMsgParser::from_bytes(context, &rawmime) {
|
||||
Err(err) => {
|
||||
warn!(context, "get_html: parser error: {:#}", err);
|
||||
Ok(None)
|
||||
}
|
||||
Ok((parser, _)) => Ok(Some(parser.html)),
|
||||
}
|
||||
} else {
|
||||
warn!(context, "get_html: no mime for {}", self);
|
||||
Ok(None)
|
||||
}
|
||||
};
|
||||
|
||||
if compressed {
|
||||
return from_rawmime(buf_decompress(&headers)?);
|
||||
}
|
||||
let headers2 = headers.clone();
|
||||
let compressed = match tokio::task::block_in_place(move || buf_compress(&headers2)) {
|
||||
Err(e) => {
|
||||
warn!(context, "get_mime_headers: buf_compress() failed: {}", e);
|
||||
return from_rawmime(headers);
|
||||
}
|
||||
Ok(o) => o,
|
||||
};
|
||||
let update = |conn: &mut rusqlite::Connection| {
|
||||
match conn.execute(
|
||||
"
|
||||
UPDATE msgs SET mime_headers=?, mime_compressed=1
|
||||
WHERE id=? AND mime_headers!='' AND mime_compressed=0",
|
||||
(compressed, self),
|
||||
) {
|
||||
Ok(rows_updated) => ensure!(rows_updated <= 1),
|
||||
Err(e) => {
|
||||
warn!(context, "get_mime_headers: UPDATE failed: {}", e);
|
||||
return Err(e.into());
|
||||
let rawmime = rawmime?;
|
||||
if !rawmime.is_empty() {
|
||||
match HtmlMsgParser::from_bytes(context, &rawmime) {
|
||||
Err(err) => {
|
||||
warn!(context, "get_html: parser error: {:#}", err);
|
||||
Ok(None)
|
||||
}
|
||||
Ok((parser, _)) => Ok(Some(parser.html)),
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
if let Err(e) = context.sql.call_write(update).await {
|
||||
warn!(
|
||||
context,
|
||||
"get_mime_headers: failed to update mime_headers: {}", e
|
||||
);
|
||||
} else {
|
||||
warn!(context, "get_html: no mime for {}", self);
|
||||
Ok(None)
|
||||
}
|
||||
from_rawmime(headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +287,6 @@ WHERE id=? AND mime_headers!='' AND mime_compressed=0",
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{self, Chat, forward_msgs, save_msgs};
|
||||
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
@@ -350,7 +304,7 @@ mod tests {
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head><body dir="auto" style="unicode-bidi: plaintext">
|
||||
</head><body>
|
||||
This message does not have Content-Type nor Subject.<br/>
|
||||
</body></html>
|
||||
"#
|
||||
@@ -368,7 +322,7 @@ This message does not have Content-Type nor Subject.<br/>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head><body dir="auto" style="unicode-bidi: plaintext">
|
||||
</head><body>
|
||||
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
|
||||
</body></html>
|
||||
"#
|
||||
@@ -387,7 +341,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head><body dir="auto" style="unicode-bidi: plaintext">
|
||||
</head><body>
|
||||
This line ends with a space and will be merged with the next one due to format=flowed.<br/>
|
||||
<br/>
|
||||
This line does not end with a space<br/>
|
||||
@@ -408,7 +362,7 @@ and will be wrapped as usual.<br/>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head><body dir="auto" style="unicode-bidi: plaintext">
|
||||
</head><body>
|
||||
mime-modified should not be set set as there is no html and no special stuff;<br/>
|
||||
although not being a delta-message.<br/>
|
||||
test some special html-characters as < > and & but also " and ' :)<br/>
|
||||
@@ -496,7 +450,6 @@ test some special html-characters as < > and & but also " and &#x
|
||||
// alice receives a non-delta html-message
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
@@ -530,7 +483,6 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = &tcm.bob().await;
|
||||
bob.allow_unencrypted().await?;
|
||||
let chat_bob = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
async fn check_receiver(ctx: &TestContext, chat: &Chat, sender: &TestContext) {
|
||||
let msg = ctx.recv_msg(&sender.pop_sent_msg().await).await;
|
||||
@@ -568,30 +520,31 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_save_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
// Alice receives a non-delta html-message
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
receive_imf(alice, raw, false).await?;
|
||||
receive_imf(&alice, raw, false).await?;
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
|
||||
// Alice saves the message
|
||||
let self_chat = alice.get_self_chat().await;
|
||||
save_msgs(alice, &[msg.id]).await?;
|
||||
save_msgs(&alice, &[msg.id]).await?;
|
||||
let saved_msg = alice.get_last_msg_in(self_chat.get_id()).await;
|
||||
assert_ne!(saved_msg.id, msg.id);
|
||||
assert_eq!(saved_msg.get_original_msg_id(alice).await?.unwrap(), msg.id);
|
||||
assert_eq!(
|
||||
saved_msg.get_original_msg_id(&alice).await?.unwrap(),
|
||||
msg.id
|
||||
);
|
||||
assert!(!saved_msg.is_forwarded()); // UI should not flag "saved messages" as "forwarded"
|
||||
assert_ne!(saved_msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(saved_msg.get_from_id(), msg.get_from_id());
|
||||
assert_eq!(saved_msg.is_dc_message, MessengerMessage::No);
|
||||
assert!(saved_msg.get_text().contains("this is plain"));
|
||||
assert!(saved_msg.has_html());
|
||||
let html = saved_msg.get_id().get_html(alice).await?.unwrap();
|
||||
let html = saved_msg.get_id().get_html(&alice).await?.unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
Ok(())
|
||||
@@ -602,7 +555,6 @@ test some special html-characters as < > and & but also " and &#x
|
||||
let mut tcm = TestContextManager::new();
|
||||
// Alice receives a non-delta html-message
|
||||
let alice = &tcm.alice().await;
|
||||
alice.allow_unencrypted().await.unwrap();
|
||||
let chat = alice
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
@@ -666,20 +618,18 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cp1252_html() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
alice,
|
||||
&t,
|
||||
include_bytes!("../test-data/message/cp1252-html.eml"),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.viewtype, Viewtype::Text);
|
||||
assert!(msg.text.contains("foo bar ä ö ü ß"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(alice).await?.unwrap();
|
||||
let html = msg.get_id().get_html(&t).await?.unwrap();
|
||||
println!("{html}");
|
||||
assert!(html.contains("foo bar ä ö ü ß"));
|
||||
Ok(())
|
||||
|
||||
236
src/imap.rs
236
src/imap.rs
@@ -6,7 +6,7 @@
|
||||
use std::{
|
||||
cmp::max,
|
||||
cmp::min,
|
||||
collections::{BTreeMap, HashMap},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
iter::Peekable,
|
||||
mem::take,
|
||||
sync::atomic::Ordering,
|
||||
@@ -21,7 +21,11 @@ use futures_lite::FutureExt;
|
||||
use ratelimit::Ratelimit;
|
||||
use url::Url;
|
||||
|
||||
use crate::chat::{self, ChatIdBlocked, add_device_msg};
|
||||
use crate::calls::{
|
||||
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
|
||||
};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, DC_VERSION_STR};
|
||||
use crate::contact::ContactId;
|
||||
@@ -30,7 +34,7 @@ use crate::ensure_and_debug_assert;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{self, Message};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::mimeparser;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
@@ -45,10 +49,6 @@ use crate::tools::{self, create_id, duration_to_str, time};
|
||||
use crate::transport::{
|
||||
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
|
||||
};
|
||||
use crate::{
|
||||
calls::{UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata},
|
||||
ephemeral::delete_expired_imap_messages,
|
||||
};
|
||||
|
||||
pub(crate) mod capabilities;
|
||||
mod client;
|
||||
@@ -525,12 +525,6 @@ impl Imap {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
|
||||
// Mark expired messages for deletion. Note that `delete_expired_imap_messages` is
|
||||
// not well optimized and should not be called before fetching.
|
||||
delete_expired_imap_messages(context, session.transport_id(), session.is_chatmail())
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
|
||||
session
|
||||
.move_delete_messages(context, watch_folder)
|
||||
.await
|
||||
@@ -1051,12 +1045,15 @@ impl Session {
|
||||
if target.is_empty() {
|
||||
self.delete_message_batch(context, &uid_set, rowid_set)
|
||||
.await
|
||||
.with_context(|| format!("cannot delete batch of messages {uid_set:?}"))?;
|
||||
.with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
|
||||
} else {
|
||||
self.move_message_batch(context, &uid_set, rowid_set, &target)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("cannot move batch of messages {uid_set:?} to folder {target:?}",)
|
||||
format!(
|
||||
"cannot move batch of messages {:?} to folder {:?}",
|
||||
&uid_set, target
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -1153,6 +1150,114 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes `\Seen` flags using `CONDSTORE` extension.
|
||||
pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
if !self.can_condstore() {
|
||||
info!(
|
||||
context,
|
||||
"Server does not support CONDSTORE, skipping flag synchronization."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if context.get_config_bool(Config::TeamProfile).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.context("Failed to select folder")?;
|
||||
if !folder_exists {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mailbox = self
|
||||
.selected_mailbox
|
||||
.as_ref()
|
||||
.with_context(|| format!("No mailbox selected, folder: {folder}"))?;
|
||||
|
||||
// Check if the mailbox supports MODSEQ.
|
||||
// We are not interested in actual value of HIGHESTMODSEQ.
|
||||
if mailbox.highest_modseq.is_none() {
|
||||
info!(
|
||||
context,
|
||||
"Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let transport_id = self.transport_id();
|
||||
let mut updated_chat_ids = BTreeSet::new();
|
||||
let uid_validity = get_uidvalidity(context, transport_id, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get UID validity for folder {folder}"))?;
|
||||
let mut highest_modseq = get_modseq(context, transport_id, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
|
||||
let mut list = self
|
||||
.uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
|
||||
.await
|
||||
.context("failed to fetch flags")?;
|
||||
|
||||
let mut got_unsolicited_fetch = false;
|
||||
|
||||
while let Some(fetch) = list
|
||||
.try_next()
|
||||
.await
|
||||
.context("failed to get FETCH result")?
|
||||
{
|
||||
let uid = if let Some(uid) = fetch.uid {
|
||||
uid
|
||||
} else {
|
||||
info!(context, "FETCH result contains no UID, skipping");
|
||||
got_unsolicited_fetch = true;
|
||||
continue;
|
||||
};
|
||||
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
|
||||
if is_seen
|
||||
&& let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
|
||||
})?
|
||||
{
|
||||
updated_chat_ids.insert(chat_id);
|
||||
}
|
||||
|
||||
if let Some(modseq) = fetch.modseq {
|
||||
if modseq > highest_modseq {
|
||||
highest_modseq = modseq;
|
||||
}
|
||||
} else {
|
||||
warn!(context, "FETCH result contains no MODSEQ");
|
||||
}
|
||||
}
|
||||
drop(list);
|
||||
|
||||
if got_unsolicited_fetch {
|
||||
// We got unsolicited FETCH, which means some flags
|
||||
// have been modified while our request was in progress.
|
||||
// We may or may not have these new flags as a part of the response,
|
||||
// so better skip next IDLE and do another round of flag synchronization.
|
||||
info!(context, "Got unsolicited fetch, will skip idle");
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
set_modseq(context, transport_id, folder, highest_modseq)
|
||||
.await
|
||||
.with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
|
||||
if !updated_chat_ids.is_empty() {
|
||||
context.on_archived_chats_maybe_noticed();
|
||||
}
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetches a list of messages by server UID.
|
||||
///
|
||||
/// Sends pairs of UID and info about each downloaded message to the provided channel.
|
||||
@@ -1182,10 +1287,9 @@ impl Session {
|
||||
|
||||
for (request_uids, set) in build_sequence_sets(&request_uids)? {
|
||||
info!(context, "Starting UID FETCH of message set \"{}\".", set);
|
||||
let mut fetch_responses = self
|
||||
.uid_fetch(&set, BODY_FULL)
|
||||
.await
|
||||
.with_context(|| format!("fetching messages {set} from folder {folder:?}"))?;
|
||||
let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
|
||||
format!("fetching messages {} from folder \"{}\"", &set, folder)
|
||||
})?;
|
||||
|
||||
// Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
|
||||
// when we want to process other messages first.
|
||||
@@ -1277,8 +1381,6 @@ impl Session {
|
||||
"Passing message UID {} to receive_imf().", request_uid
|
||||
);
|
||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
|
||||
|
||||
// If there was an error receiving the message, show a device message:
|
||||
let received_msg = match res {
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf error: {err:#}.");
|
||||
@@ -1894,29 +1996,80 @@ pub(crate) async fn prefetch_should_download(
|
||||
// prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
|
||||
// (prevent_rename is the last argument of from_field_to_contact_id())
|
||||
|
||||
// New SecureJoin is fully encrypted,
|
||||
// but for compatibility we still download legacy `Secure-Join: vc-request` messages.
|
||||
let is_legacy_securejoin = headers.get_header_value(HeaderDef::SecureJoin).is_some();
|
||||
|
||||
let is_encrypted = headers
|
||||
.get_header_value(HeaderDef::ContentType)
|
||||
.is_some_and(|content_type| {
|
||||
mailparse::parse_content_type(&content_type).mimetype == "multipart/encrypted"
|
||||
});
|
||||
|
||||
if flags.any(|f| f == Flag::Draft) {
|
||||
info!(context, "Ignoring draft message");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let should_download = maybe_ndn
|
||||
|| (!blocked_contact
|
||||
&& (is_legacy_securejoin
|
||||
|| is_encrypted
|
||||
|| !context.get_config_bool(Config::ForceEncryption).await?));
|
||||
let should_download = !blocked_contact || maybe_ndn;
|
||||
Ok(should_download)
|
||||
}
|
||||
|
||||
/// Marks messages in `msgs` table as seen, searching for them by UID.
|
||||
///
|
||||
/// Returns updated chat ID if any message was marked as seen.
|
||||
async fn mark_seen_by_uid(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
folder: &str,
|
||||
uid_validity: u32,
|
||||
uid: u32,
|
||||
) -> Result<Option<ChatId>> {
|
||||
if let Some((msg_id, chat_id)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id, chat_id FROM msgs
|
||||
WHERE id > 9 AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM imap
|
||||
WHERE transport_id=?
|
||||
AND folder=?
|
||||
AND uidvalidity=?
|
||||
AND uid=?
|
||||
LIMIT 1
|
||||
)",
|
||||
(transport_id, &folder, uid_validity, uid),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let chat_id: ChatId = row.get(1)?;
|
||||
Ok((msg_id, chat_id))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
|
||||
{
|
||||
let updated = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=?1
|
||||
WHERE (state=?2 OR state=?3)
|
||||
AND id=?4",
|
||||
(
|
||||
MessageState::InSeen,
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to update msg {msg_id} state"))?
|
||||
> 0;
|
||||
|
||||
if updated {
|
||||
msg_id
|
||||
.start_ephemeral_timer(context)
|
||||
.await
|
||||
.with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
|
||||
Ok(Some(chat_id))
|
||||
} else {
|
||||
// Message state has not changed.
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
// There is no message is `msgs` table matching the given UID.
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule marking the message as Seen on IMAP by adding all known IMAP messages corresponding to
|
||||
/// the given Message-ID to `imap_markseen` table.
|
||||
pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
|
||||
@@ -2014,6 +2167,17 @@ pub(crate) async fn set_modseq(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
|
||||
(transport_id, folder),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
|
||||
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
|
||||
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
|
||||
|
||||
@@ -17,6 +17,10 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc2087>
|
||||
pub can_check_quota: bool,
|
||||
|
||||
/// True if the server has CONDSTORE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc7162>
|
||||
pub can_condstore: bool,
|
||||
|
||||
/// True if the server has METADATA capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc5464>
|
||||
pub can_metadata: bool,
|
||||
|
||||
@@ -66,6 +66,7 @@ pub(crate) async fn determine_capabilities(
|
||||
can_idle: caps.has_str("IDLE"),
|
||||
can_move: caps.has_str("MOVE"),
|
||||
can_check_quota: caps.has_str("QUOTA"),
|
||||
can_condstore: caps.has_str("CONDSTORE"),
|
||||
can_metadata: caps.has_str("METADATA"),
|
||||
can_compress: caps.has_str("COMPRESS=DEFLATE"),
|
||||
can_push: caps.has_str("XDELTAPUSH"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user